linux2.2系统编程-进程管理

​ 每个进程都有独立的地址空间、系统资源(如文件描述符、信号处理器等)。因此,进程之间的切换开销较大,因为需要保存当前进程的状态,并加载下一个进程的状态。进程的实现并不依赖多核并行计算,而是通过时间片实现。时间片是指通过时分复用为每个进程分配较为完整的虚拟内存。

线程是对进程的分割,避免进程的资源浪费。线程是轻量级的,它属于某个进程,并且与同一进程中的其他线程共享大多数资源(如内存地址空间、打开的文件等),但每个线程有自己的执行上下文(如寄存器状态、堆栈指针等)。由于线程间共享资源,线程间的切换比进程间的切换要快得多。

1 进程与线程的区别

进程(Process)和线程(Thread)都是操作系统进行运算调度的基本单位,但它们之间有几个关键的区别:

资源拥有

  • 进程:每个进程都有独立的地址空间、系统资源(如文件描述符、信号处理器等)。因此,进程之间的切换开销较大,因为需要保存当前进程的状态,并加载下一个进程的状态。
  • 线程:线程是轻量级的,它属于某个进程,并且与同一进程中的其他线程共享大多数资源(如内存地址空间、打开的文件等),但每个线程有自己的执行上下文(如寄存器状态、堆栈指针等)。由于线程间共享资源,线程间的切换比进程间的切换要快得多。

独立性

  • 进程:一个进程的执行不会直接受到其他进程的影响。如果一个进程崩溃,通常不会影响到其他进程。
  • 线程:线程不是完全独立的,同属一个进程的所有线程共享该进程的资源。如果一个线程出现错误,可能会导致整个进程失败。

通信

  • 进程:进程间通信(IPC, Inter-Process Communication)较为复杂,可能需要使用专门的机制如管道、套接字、消息队列或共享内存以及信号等来实现进程间的数据交换,特殊的信号量与文件锁理论上也可用于进程间通信,但不是首选
  • 线程:由于线程共享同一个进程的地址空间,它们可以直接通过读写相同的内存区域来进行通信,这使得线程间通信更为简单高效。

2 进程控制块

进程控制块(Process Control Block,简称PCB)是操作系统用来管理进程的数据结构。每个进程都有一个与之关联的PCB,它包含了操作系统需要知道的关于该进程的所有信息。PCB对于进程的创建、调度、同步和终止等操作至关重要。

PCB中通常包含的信息有:

  1. 标识信息

    • 进程ID(PID):用于唯一标识一个进程,父进程(PPID)。
    • 用户ID(UID)和组ID(GID):表示拥有该进程的用户及其所属组。
  2. 状态信息

    • 基本状态:可以是创建、就绪、运行、等待(阻塞)或终止。这反映了进程当前是否在CPU上执行、准备执行还是等待某些事件发生。

      • 创建态:系统正在为其分配内存等资源,未进入就绪态。

      • 就绪态:等待调度器分配CPU进行计算。

      • 运行态:CPU执行计算。
      • 阻塞态:进程由于等待中断等事件被挂起,期间基本不消耗CPU计算资源。
      • 终止态:进程结束或被杀死,系统回收资源时。

      image-20250102135311699

  3. 查看PCB

1
cat/proc/<pid>/status

PCB的作用

  • 进程管理:操作系统通过PCB来管理和跟踪系统中的所有进程。
  • 进程切换:当操作系统决定切换到另一个进程时,它会保存当前进程的PCB,并加载新进程的PCB以恢复其执行环境。
  • 调度决策:调度器根据PCB中的信息(如优先级)来选择下一个应该运行的进程。
  • 资源分配与回收:PCB帮助操作系统确定为进程分配哪些资源以及何时回收这些资源。

3 Linux进程管理命令

3.1 ps -auxps -ef(简略输出)

1
2
3
4
5
6
serenitatis@shumeipai:~/linux-project $ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 168692 11284 ? Ss 09:12 0:03 /sbin/init splash
root 2 0.0 0.0 0 0 ? S 09:12 0:00 [kthreadd]
serenit+ 4724 0.0 0.0 5408 1536 ? S 14:13 0:00 sleep 180
serenit+ 4744 100 0.0 11532 4352 pts/0 R+ 14:16 0:00 ps -aux
  1. USER

    • 显示运行该进程的用户名称。这通常是启动进程的用户账号。
  2. PID (Process ID)

    • 每个进程都有一个唯一的标识符,称为进程ID(PID)。这是操作系统用来识别和管理各个进程的关键值。
  3. %CPU

    • 表示进程占用的CPU时间百分比。它反映了自上次更新以来,该进程使用的CPU资源占总可用CPU资源的比例。
  4. %MEM

    • 显示进程使用的物理内存百分比。它表示的是相对于系统总RAM大小,该进程所占的份额。
  5. VSZ (Virtual Memory Size)

    • 进程虚拟内存大小,以KB为单位。这包括了程序代码、数据段以及所有已映射文件的大小。注意,这个值可能比实际使用的物理内存量要大,因为它还包含了未分配的虚拟地址空间。
  6. RSS (Resident Set Size)

    • 实际驻留在主存中的进程内存大小,以KB为单位。RSS是进程当前正在使用的物理内存量,不包括已经被交换出去的部分。
  7. TTY (Teletype)

    • 如果进程是通过终端启动的,则显示与之关联的终端设备名(例如,pts/0, tty1等)。对于守护进程或其他非交互式进程,这里可能会显示”?”或”-“.
  8. STAT (Status)

    • 进程的状态代码。常见的状态有:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      进程状态详解

      D: 不可中断的休眠态
      进程处于等待某个资源(通常是 I/O 操作)的状态,此时无法被中断,直到资源可用。

      R: 运行态和就绪态
      进程正在运行或者可以运行(在就绪队列中等待 CPU 资源)。

      S: 可中断的休眠态
      进程正在等待某个事件完成,可以被信号中断。

      T: 停止(暂停)状态
      进程被作业控制信号停止,例如通过 `Ctrl+Z` 暂停。

      t: 被调试器暂停
      进程在调试期间被调试器暂停。

      W: 分页状态(自 2.6 内核起不再有效)
      过去用于表示进程在等待分页,但该状态已过时。

      X: 死亡态
      进程已经死亡,这个状态几乎不会看到,因为它会很快消失。

      Z: 僵尸态
      进程已终止,但其父进程尚未回收它的资源,导致它以僵尸状态存在。

      ### 进程的附加状态

      <: 优先级较高
      进程的优先级较高,可能会抢占其他进程的资源,对其他用户不友好。

      N: 优先级较低
      进程的优先级较低,运行时对其他用户较为友好。

      L: 内存锁定
      进程的某些内存页被锁定在内存中,通常用于实时操作或自定义 I/O 操作。

      s: 会话组长
      进程是会话组的组长,负责管理一组相关进程。

      l: 多线程进程
      进程中包含多个线程,通常使用 `CLONE_THREAD` 实现,如 NPTL pthreads。

      +: 前台进程
      进程属于前台进程组,通常与用户交互较多。
  1. START

    • 显示进程开始的时间。可以是日期或者具体的启动时间戳。
  2. TIME

    • 累计CPU时间,即从进程启动至今消耗的CPU时间总量,格式为分钟:秒数。
  3. COMMAND

    • 显示启动进程的命令行及其参数。有时为了节省空间,较长的命令可能会被截断;在这种情况下,你可以使用其他方法(如ps -f)来查看完整的命令行。

​ 一般结合grpe使用

1
2
3
ps -aux |grep <name>
另外如需查看父节点
ps -ef |grep <name>

3.2 htop类windows任务管理器

image-20250102143959337

3.3 kill向进程发送信号

1
2
3
4
5
6
7
kill
kill -l 查看进程中的信号
kill -2 pid 中止信号 ctrl + c
kill -9 pid 杀死进程
kill -19 pid 让进程暂停
kill -18 pid 让暂停的进程继续执行
killall a.out 杀死系统中所有名叫 a.out 的进程

4 Linux进程管理c接口

4.1 fork创建新子进程

​ fork创建子进程,先复制父进程复制完成后是独立的进程。

1
2
3
4
5
6
7
8
9
10
SYNOPSIS
#include <unistd.h>

pid_t fork(void);
if succeed
return to parent :child's pid
return to child '0'
if fail
return to parent : -1

注意创建子进程得到的数量

image-20250102154948217

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
#include <stdio.h>

int main()
{
pid_t pid = 0;
for(int i=0 ;i<2;i++)
{
fork();
printf("#");
}
}

输出结果8个#

4.2 父子进程的内存拷贝

在Linux系统中,当一个父进程通过fork系统调用创建子进程时,子进程会获得父进程的一个拷贝。这种拷贝遵循“写时拷贝”的原则。这意味着在创建子进程时,父进程和子进程最初共享相同的内存空间,直到其中一个进程试图修改内存内容时,才会进行实际的内存拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char const *argv[])
{
int num = 1234;
pid_t pid = 0;

if(-1 == (pid = fork()))
{
puts("创建失败");
return -1;
}
else if(0 == pid)
{
printf("我是子进程..\n");
printf("子进程 num = %d\n" , num);
num = 80; // 写实拷贝 映射到不同的内存空间
sleep(2);
printf("子进程 num = %d\n" , num);
}
else if(0 < pid){
printf("我是父进程..\n");
printf("父进程 num = %d\n" , num);
sleep(1);
printf("父进程 num = %d\n" , num);
}

puts("结束");

return 0;
}

output:

1
2
3
4
5
6
7
8
我是父进程..
父进程 num = 1234
我是子进程..
子进程 num = 1234 //更改前
父进程 num = 1234
结束
子进程 num = 80//复制内存空间后
结束

4.3 查看当前PID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NAME
getpid, getppid - get process identification

LIBRARY
Standard C library (libc, -lc)

SYNOPSIS
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

DESCRIPTION
getpid() returns the process ID (PID) of the calling process.

getppid() returns the process ID of the parent of the calling process.
This will be either the ID of the process that created this process using
fork(), or, if that process has already terminated, the ID of the process
to which this process has been reparented (either init(1) or a "subreaper"
process defined via the prctl(2) PR_SET_CHILD_SUBREAPER operation).

example :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char const *argv[])
{
int num = 1234;
pid_t pid = 0;

if(-1 == (pid = fork()))
{
puts("创建失败");
return -1;
}
else if(0 == pid)
{
printf("PID of parent:%d\n",getpid());
}
else if(0 < pid){

printf("pid of child %d\n",getpid());

}
while(1);


return 0;
}

output:

1
2
3
4
5
6
7
8
pid of parent:20147
PID of child:20146
serenitatis@shumeipai:~/myTool $ ps -ef | grep a.out
UID PID PPID C STIME TTY TIME CMD
serenit+ 20146 19051 99 19:12 pts/2 00:00:26 ./a.out
serenit+ 20147 20146 99 19:12 pts/2 00:00:26 ./a.out
serenit+ 20229 16477 0 19:12 pts/1 00:00:00 grep --color=auto a.out

一般来说子进程的pid会在父节点的基础上加1。

4.4 主动退出进程exit

exit是直接退出当前进程,不区别是子进程还是父进程。所以可能会造孤儿节点,孤儿节点只能由系统进程(PID = 1)回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main() {
pid_t pid = fork(); // 创建子进程

if (pid < 0) {
// fork 失败
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process (PID = %d, Parent PID = %d)\n", getpid(), getppid());
sleep(5); // 子进程睡眠 5 秒
printf("Child process (PID = %d) now has parent PID = %d\n", getpid(), getppid());
exit(0);
} else {
// 父进程
printf("Parent process (PID = %d)\n", getpid());
sleep(1); // 父进程睡眠 1 秒后退出
printf("Parent process exiting...\n");
exit(0);
}

return 0;
}

4.5 回收子进程wait

waitwaitpid 是用于回收子进程的系统调用,主要用于父进程等待子进程结束并获取其退出状态。

4.5.1 wait 函数

  • 功能: 等待任意一个子进程结束。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    NAME
    wait, waitpid, waitid - wait for process to change state

    LIBRARY
    Standard C library (libc, -lc)

    SYNOPSIS
    #include <sys/wait.h>

    pid_t wait(int *_Nullable wstatus);
    pid_t wait(int *_Nullable wstatus);
    pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);

    int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
    /* This is the glibc and POSIX interface; see
    NOTES for information on the raw system call. */
  • 参数:

    • status: 用于存储子进程的退出状态。
  • 返回值:

    • 成功时返回结束的子进程的 PID。
    • 失败时返回 -1。
  • 特点:

    • 如果没有子进程结束,父进程会阻塞。
    • 如果有多个子进程,任意一个结束都会返回。

4.5.2 waitpid 函数

  • 功能: 等待指定的子进程结束。
  • 参数:
    • pid:
      • > 0: 等待指定 PID 的子进程。
      • -1: 等待任意子进程,类似 wait
      • 0: 等待与父进程同组 ID 的任何子进程。
      • < -1: 等待组 ID 等于 pid 绝对值的任何子进程。
    • status: 用于存储子进程的退出状态。
    • options: 控制行为,常用选项:
      • WNOHANG: 非阻塞模式,没有子进程结束时立即返回。
      • WUNTRACED: 返回已停止的子进程状态。
  • 返回值:
    • 成功时返回结束的子进程的 PID。
    • 如果使用 WNOHANG 且没有子进程结束,返回 0。
    • 失败时返回 -1。
  • 特点:
    • 可以指定等待的子进程。
    • 支持非阻塞模式。

4.5.3 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>

int main() {
pid_t pid = fork();

if (pid == 0) {
// 子进程
printf("Child process\n");
sleep(2);
_exit(0);
} else if (pid > 0) {
// 父进程
int status;
pid_t child_pid = wait(&status);
if (WIFEXITED(status)) {
printf("Child %d exited with status %d\n", child_pid, WEXITSTATUS(status));
}
} else {
perror("fork");
return 1;
}

return 0;
}

4.5.4 总结

  • wait 用于等待任意子进程结束。
  • waitpid 更灵活,可以指定子进程并支持非阻塞模式。
  • 两者都用于回收子进程资源,防止僵尸进程。

4.6 exe函数簇利用外部应用创建进程

exec 函数簇是一组用于替换当前进程映像的系统调用。它们会将当前进程的代码段、数据段、堆栈等替换为新的程序文件,并从新程序的入口点开始执行。与 fork 不同,exec 不会创建新进程,而是替换当前进程的内容。

exec 函数簇通常与 fork 结合使用,用于创建新进程并运行外部程序。


4.6.1 exec 函数簇的常见函数

以下是 exec 函数簇的常见函数:

函数名 描述
execl 参数列表形式,需提供完整路径名。
execlp 参数列表形式,会在 PATH 环境变量中查找可执行文件。
execle 参数列表形式,可指定环境变量。
execv 参数数组形式,需提供完整路径名。
execvp 参数数组形式,会在 PATH 环境变量中查找可执行文件。
execvpe 参数数组形式,可指定环境变量。

4.6.2 函数原型

1
2
3
4
5
6
7
8
#include <unistd.h>

int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /* (char *) NULL, char *const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

4.6.3 参数说明

  • path: 可执行文件的完整路径。
  • file: 可执行文件名,会在 PATH 环境变量中查找。
  • arg: 程序的命令行参数,第一个参数通常是程序名。
  • argv: 参数数组,以 NULL 结尾。
  • envp: 环境变量数组,以 NULL 结尾。

4.6.4 返回值

  • 成功时,exec 函数不会返回(因为当前进程的映像已被替换)。
  • 失败时,返回 -1,并设置 errno

4.6.5 示例代码

4.6.5.1 示例 1:使用 execl 运行 ls 命令
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
#include <stdio.h>

int main() {
printf("Before exec\n");

// 使用 execl 运行 ls 命令
execl("/bin/ls", "ls", "-l", NULL);

// 如果 exec 成功,以下代码不会执行
perror("execl failed");
return 1;
}
4.6.5.2 示例 2:使用 execvp 运行 ls 命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include <stdio.h>

int main() {
printf("Before exec\n");

// 使用 execvp 运行 ls 命令
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);

// 如果 exec 成功,以下代码不会执行
perror("execvp failed");
return 1;
}
4.6.5.3 示例 3:结合 forkexec 创建新进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork();

if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process (PID = %d)\n", getpid());

// 使用 execlp 运行 ls 命令
execlp("ls", "ls", "-l", NULL);

// 如果 exec 成功,以下代码不会执行
perror("execlp failed");
return 1;
} else {
// 父进程
printf("Parent process (PID = %d)\n", getpid());
wait(NULL); // 等待子进程结束
printf("Child process finished\n");
}

return 0;
}

4.6.6 关键点

  1. exec 不会创建新进程
    • 它只是替换当前进程的映像。
    • 如果需要创建新进程,通常先调用 fork,然后在子进程中调用 exec
  2. 参数传递
    • 使用 execlexecle 时,参数以列表形式传递。
    • 使用 execvexecvp 时,参数以数组形式传递。
  3. 环境变量
    • execleexecvpe 可以指定环境变量数组。
    • 其他函数会继承当前进程的环境变量。
  4. 错误处理
    • 如果 exec 失败,当前进程会继续执行,因此需要检查返回值并处理错误。

4.6.7 总结

  • exec 函数簇用于替换当前进程的映像,运行外部程序。
  • 通常与 fork 结合使用,创建新进程并运行外部程序。
  • 不同的 exec 函数适用于不同的场景(如是否搜索 PATH、是否指定环境变量等)。

4.7 popen管道回收外部应用的返回值

popen 是一个标准库函数,用于创建一个管道并启动一个外部应用程序。通过 popen,你可以与外部应用程序进行通信,并读取其输出或向其输入数据。popen 的返回值是一个文件指针(FILE*),可以通过标准 I/O 函数(如 freadfgets 等)读取或写入数据。

popen 特别适合用于执行命令行工具并捕获其输出。


4.7.1 popen 函数原型

1
2
3
4
#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

4.7.2 参数说明

  • command
    • 要执行的命令行命令。
    • 例如:"ls -l""grep hello" 等。
  • type
    • 指定管道的类型:
      • "r":以读模式打开管道,读取外部应用程序的输出。
      • "w":以写模式打开管道,向外部应用程序输入数据。
  • 返回值
    • 成功时,返回一个 FILE* 指针,用于读取或写入数据。
    • 失败时,返回 NULL,并设置 errno

4.7.3 pclose 函数

  • 功能:关闭由 popen 打开的管道,并等待外部应用程序结束。
  • 参数
    • streampopen 返回的 FILE* 指针。
  • 返回值
    • 成功时,返回外部应用程序的退出状态。
    • 失败时,返回 -1

4.7.4 示例代码

4.7.4.1 示例 1:读取外部应用程序的输出

以下代码使用 popen 执行 ls -l 命令,并读取其输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>

int main() {
FILE *fp;
char buffer[1024];

// 使用 popen 执行 ls -l 命令
fp = popen("ls -l", "r");
if (fp == NULL) {
perror("popen failed");
return 1;
}

// 读取命令输出
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}

// 关闭管道并获取命令的退出状态
int status = pclose(fp);
if (status == -1) {
perror("pclose failed");
} else {
printf("Command exited with status: %d\n", WEXITSTATUS(status));
}

return 0;
}

4.7.4.2 示例 2:向外部应用程序输入数据

以下代码使用 popengrep 命令输入数据,并读取其输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>

int main() {
FILE *fp;
char buffer[1024];

// 使用 popen 执行 grep hello 命令
fp = popen("grep hello", "w");
if (fp == NULL) {
perror("popen failed");
return 1;
}

// 向 grep 输入数据
fprintf(fp, "hello world\n");
fprintf(fp, "goodbye\n");
fprintf(fp, "hello again\n");

// 关闭管道并获取命令的退出状态
int status = pclose(fp);
if (status == -1) {
perror("pclose failed");
} else {
printf("Command exited with status: %d\n", WEXITSTATUS(status));
}

return 0;
}

4.7.5 关键点

  1. popen 的返回值
    • 返回一个 FILE* 指针,可以通过标准 I/O 函数读取或写入数据。
  2. pclose 的作用
    • 关闭管道并等待外部应用程序结束。
    • 返回外部应用程序的退出状态。
  3. 管道类型
    • "r":读取外部应用程序的输出。
    • "w":向外部应用程序输入数据。
  4. 错误处理
    • 如果 popenpclose 失败,会返回 NULL-1,并设置 errno

4.7.6 总结

  • popen 提供了一种简单的方式与外部应用程序通信。
  • 通过 popen,可以读取外部应用程序的输出或向其输入数据。
  • pclose 用于关闭管道并获取外部应用程序的退出状态。
  • popenpclose 是处理命令行工具输出的强大工具,适合需要与外部程序交互的场景。

4.8 常用的进程检查宏

在 Linux 系统中,进程的退出状态可以通过 waitwaitpid 函数获取。为了检查进程的退出状态,C 标准库提供了一组宏,用于解析进程的退出状态。这些宏定义在 <sys/wait.h> 头文件中。

以下是常用的进程检查宏及其作用:


4.9 常用进程检查宏

宏名 描述
WIFEXITED(status) 检查进程是否正常退出(通过 exit_exit 退出)正常退出,返回非零值(true)。
WEXITSTATUS(status) 如果进程正常退出,获取进程的退出状态(exit_exit 的参数)。
WIFSIGNALED(status) 检查进程是否因信号而终止。
WTERMSIG(status) 如果进程因信号终止,获取导致进程终止的信号编号。
WIFSTOPPED(status) 检查进程是否处于停止状态(例如,被 SIGSTOPSIGTSTP 信号停止)。
WSTOPSIG(status) 如果进程处于停止状态,获取导致进程停止的信号编号。
WIFCONTINUED(status) 检查进程是否从停止状态恢复(收到 SIGCONT 信号)。

4.9.1 示例代码

以下代码演示了如何使用这些宏检查子进程的退出状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork();

if (pid < 0) {
perror("fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process (PID = %d)\n", getpid());
sleep(2); // 模拟子进程运行
exit(42); // 子进程退出,状态为 42
} else {
// 父进程
int status;
wait(&status); // 等待子进程结束

if (WIFEXITED(status)) {
printf("Child exited normally with status: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child terminated by signal: %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("Child stopped by signal: %d\n", WSTOPSIG(status));
} else if (WIFCONTINUED(status)) {
printf("Child continued\n");
}
}

return 0;
}

output:

1
2
Child process (PID = 1234)
Child exited normally with status: 42

4.9.2 总结

  • 进程检查宏是解析进程退出状态的重要工具。
  • 通过结合 waitwaitpid 使用这些宏,可以获取进程的退出状态、终止信号等信息。
  • 这些宏在编写多进程程序时非常有用,尤其是在需要处理子进程的退出状态时。

4.10 守护进程

守护进程(Daemon Process)是在后台运行的一种特殊进程,通常用于执行系统任务或服务,而不与任何终端或用户交互。守护进程在 Linux 系统中非常常见,例如网络服务(如 httpd)、日志服务(如 syslogd)等。

4.10.1 守护进程的特点

  1. 脱离终端

    • 守护进程不与任何终端关联,因此不会受到终端关闭的影响。
  2. 后台运行

    • 守护进程在后台运行,不会占用终端输入输出。
  3. 生命周期长

    • 守护进程通常从系统启动时开始运行,直到系统关闭。
  4. 无控制终端

    • 守护进程没有控制终端,因此不会接收来自终端的信号(如 SIGINTSIGHUP 等)。
  5. 以 root 权限运行

    • 许多守护进程需要以 root 权限运行,以便访问系统资源或执行特权操作。

4.10.2 创建守护进程的步骤

以下是创建一个守护进程的标准步骤:

4.10.2.1 . 调用 fork 创建子进程
  • 父进程退出,子进程继续运行。
  • 这样可以使子进程脱离终端。
4.10.2.2 . 调用 setsid 创建新会话
  • 子进程调用 setsid 创建一个新的会话,并成为该会话的组长。
  • 这会使子进程脱离控制终端。
4.10.2.3 . 再次调用 fork(可选)
  • 再次调用 fork 创建孙子进程,并退出子进程。
  • 这样可以确保孙子进程不是会话组长,从而无法重新获取控制终端。
  • 会话由一个或多个进程组成,会话组组长的生命周期决定了会话的生命周期。
4.10.2.4 . 更改工作目录
  • 将工作目录更改为根目录(/)或其他安全目录。
  • 这样可以避免守护进程占用挂载的文件系统。
4.10.2.5 . 重设文件权限掩码
  • 调用 umask(0) 重设文件权限掩码。
  • 这样可以确保守护进程创建的文件具有预期的权限。
4.10.2.6 . 关闭文件描述符
  • 关闭所有不需要的文件描述符(如标准输入、标准输出、标准错误)。
  • 这样可以释放资源并避免干扰。
4.10.2.7 . 处理信号
  • 忽略或处理某些信号(如 SIGHUPSIGTERM 等),以确保守护进程不会被意外终止。

4.10.3 示例代码

以下是一个简单的守护进程实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>

void daemonize() {
// 1. 创建子进程
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid > 0) {
// 父进程退出
exit(0);
}

// 2. 创建新会话
if (setsid() < 0) {
perror("setsid failed");
exit(1);
}

// 3. 再次调用 fork
pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid > 0) {
// 子进程退出
exit(0);
}

// 4. 更改工作目录
if (chdir("/") < 0) {
perror("chdir failed");
exit(1);
}

// 5. 重设文件权限掩码
umask(0);

// 6. 关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

// 7. 重定向标准输入、输出、错误到 /dev/null
open("/dev/null", O_RDONLY); // stdin
open("/dev/null", O_RDWR); // stdout
open("/dev/null", O_RDWR); // stderr
}

int main() {
// 守护进程化
daemonize();

// 守护进程的主逻辑
while (1) {
// 模拟守护进程的工作
sleep(1);
}

return 0;
}

4.10.4 守护进程的管理

  1. 启动守护进程

    • 守护进程通常通过系统启动脚本(如 /etc/init.dsystemd 服务)启动。
  2. 停止守护进程

    • 通过发送信号(如 SIGTERM)停止守护进程。
    • 例如:kill <pid>
  3. 查看守护进程

    • 使用 ps 命令查看守护进程:
      1
      ps -ef | grep <daemon_name>
  4. 日志记录

    • 守护进程通常将日志写入文件(如 /var/log 目录)。
    • 可以使用 syslog 函数将日志发送到系统日志服务。

4.10.5 守护进程的日志记录

以下是一个使用 syslog 记录日志的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <syslog.h>

int main() {
// 打开日志
openlog("mydaemon", LOG_PID, LOG_DAEMON);

// 记录日志
syslog(LOG_INFO, "Daemon started");

// 守护进程的主逻辑
while (1) {
syslog(LOG_INFO, "Daemon is running");
sleep(10);
}

// 关闭日志
closelog();

return 0;
}

4.10.6 总结

  • 守护进程是后台运行的特殊进程,通常用于执行系统任务或服务。
  • 创建守护进程的步骤包括:forksetsid、更改工作目录、重设文件权限掩码、关闭文件描述符等。
  • 守护进程的管理包括启动、停止、查看和日志记录。
  • 守护进程是 Linux 系统中实现后台服务的重要机制。

测验

写一个多进程 的程序, 用于拷贝文件

假设 一个文件为 100字节,在主进程中 计算 文件的大小 100,分两个进程 ,进程 A 拷贝 0-49,进程 B 拷贝 50-99。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "head.h"

int main(int argc, char const *argv[])
{
int fd_001 = open("f001.txt",O_RDONLY);

if(fd_001 == -1)
{
ERRLOG("123");
return -1;
}
int filelen = 0;
char buff[5];
memset(buff,0,sizeof(buff));
int byte = 0;
lseek(fd_001,0,SEEK_SET);

while(byte = read(fd_001,buff,sizeof(buff)))
{
filelen+=byte;

}
printf("filelen:%d\n",filelen);
close(fd_001);


pid_t pid = 1;
pid = fork();
if (pid < 0)
{
ERRLOG("fork");
return -1;
}
if(pid == 0)
{
int fd_001 = open("f001.txt",O_RDONLY);
int fd_002 = open("f002.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
int fd_003 = open("f003.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
lseek(fd_001,0,SEEK_SET);
int i=0;
printf("process one start\n");
for(i=0;i<filelen/2;i+=5)
{
read(fd_001,buff,sizeof(buff));
write(fd_002,buff,sizeof(buff));
memset(buff,0,sizeof(buff));

}
printf("onelen:%d\n",i);

close(fd_001);
close(fd_002);
close(fd_003);

printf("我是子进程1 我退出了\n");
exit(0);
}else
{
int fd_001 = open("f001.txt",O_RDONLY);
int fd_002 = open("f002.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
int fd_003 = open("f003.txt",O_RDWR|O_CREAT|O_TRUNC,0666);
pid_t pidb = 1;
pidb = fork();
if(pidb < 0)
{
ERRLOG("fork");
return -1;
}
if(pidb == 0)
{
lseek(fd_001,filelen/2,SEEK_SET);
int i=0;
printf("process two start\n");
for(i=0;i<filelen/2;i+=5)
{
read(fd_001,buff,sizeof(buff));
write(fd_003,buff,sizeof(buff));
memset(buff,0,sizeof(buff));

}
printf("twolen:%d\n",i);

close(fd_001);
close(fd_002);
close(fd_003);
printf("我是子进程2 我退出了\n");
}

}
wait(NULL);
wait(NULL);


return 0;
}

output:

image-20250103203321710

5 线程管理

5.1 线程的创建

新建线程pthread_create

1
2
3
4
5
6
7
8
9
10
11
12
13
NAME
pthread_create - create a new thread

LIBRARY
POSIX threads library (libpthread, -lpthread)

SYNOPSIS
#include <pthread.h>

int pthread_create(pthread_t *restrict thread,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg);
  1. 参数说明

(1) pthread_t *thread

  • 作用:用于存储新线程的标识符(线程 ID)。
  • 类型pthread_t 是一个不透明的数据类型,用于唯一标识一个线程。
  • 注意:线程 ID 由系统分配,创建成功后可以通过该 ID 操作线程。

(2)const pthread_attr_t *attr

  • 作用:用于设置线程的属性(如栈大小、调度策略等)。
  • 类型pthread_attr_t 是一个结构体,用于描述线程的属性。
  • 注意
    • 如果传入 NULL,则使用默认属性。
    • 如果需要自定义属性,可以使用 pthread_attr_init 初始化属性对象,并通过 pthread_attr_setxxx 系列函数设置属性。

(3) void *(*start_routine) (void *)

  • 作用:线程启动后执行的函数。
  • 类型:函数指针,指向一个返回 void * 并接受 void * 参数的函数。
  • 注意
    • 该函数是线程的入口点,线程启动后会立即执行该函数。
    • 函数的返回值可以通过 pthread_join 获取。

(4) void *arg

  • 作用:传递给 start_routine 函数的参数。
  • 类型void *,可以指向任意类型的数据。
  • 注意
    • 如果需要传递多个参数,可以将它们封装到一个结构体中,然后传递结构体的指针。
    • 确保参数的生命周期在线程使用期间有效。

返回值

  • 成功:返回 0
  • 失败:返回错误码(非零值),常见的错误码包括:
    • EAGAIN:系统资源不足,无法创建线程。
    • EINVAL:无效的线程属性。
    • EPERM:没有权限设置调度策略或参数。

等待线程pthread_join

结束线程pthread_exit

测验

多线程拷贝文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include  "head.h"
#define FILE_SOURCE "f001.txt"
#define FILE_DEST_ONE "f002.txt"
#define FILE_DEST_TWO "f003.txt"


void *thread_func_one(void *arg)
{
printf("this is thread one\n");
int fd_001 = open("f001.txt",O_RDONLY);
int fd_002 = open("f002.txt",O_RDWR|O_CREAT|O_TRUNC);
int readbuff[50];
int readsize = 0;
lseek(fd_001,0,SEEK_SET);
memset(readbuff,0,50);
read(fd_001,readbuff,50);
write(fd_002,readbuff,50);

close(fd_001);
close(fd_002);
sleep(1);
time_t timeNew = time(NULL);
printf("time1:%ld\n",timeNew);
printf("one exit\n");


}
void *thread_func_two(void *arg)
{
printf("this is thread two\n");
int fd_001 = open("f001.txt",O_RDONLY);
int fd_003 = open("f003.txt",O_RDWR|O_CREAT|O_TRUNC);
int readbuff[50];
int readsize = 0;
lseek(fd_001,50,SEEK_SET);
memset(readbuff,0,50);
read(fd_001,readbuff,50);
write(fd_003,readbuff,50);

close(fd_001);
close(fd_003);
sleep(1);
time_t timeNew = time(NULL);
printf("time2:%ld\n",timeNew);
printf("two exit\n");
}



int main()
{

int fd_001 = open("f001.txt",O_RDONLY);


if(fd_001 == -1)
{
ERRLOG("123");
return -1;
}
int filelen = 0;
char buff[5];
memset(buff,0,sizeof(buff));
int byte = 0;
lseek(fd_001,0,SEEK_SET);

while(byte = read(fd_001,buff,sizeof(buff)))
{
filelen+=byte;
}
printf("filelen:%d\n",filelen);
close(fd_001);



pthread_t thread_one;
if(pthread_create(&thread_one,NULL,thread_func_one,NULL))
{
ERRLOG("create err");
}


pthread_t thread_two;
if(pthread_create(&thread_two,NULL,thread_func_two,NULL))
{
ERRLOG("create err");
}
pthread_join(thread_one,NULL);
pthread_join(thread_two,NULL);

}

output

1
2
3
4
5
6
7
filelen:100
this is thread one
this is thread two
time2:1735907369
two exit
time1:1735907369
one exit

image-20250103203321710

6 主要的线程间通信方法

线程间通信:

通信方式 描述 适用场景
共享变量 通过全局变量或堆内存通信 简单的数据共享
互斥锁 保护共享资源 临界区保护
条件变量 线程间的条件同步 复杂的线程同步
信号量 控制对共享资源的访问 资源计数或互斥
屏障 同步多个线程 多线程协同工作
读写锁 允许多读单写 读多写少的场景
自旋锁 忙等待的锁 短时间的锁竞争
消息队列 通过消息队列传递数据 线程间数据传递
管道 通过管道传递数据 线程间数据传递
文件描述符 通过文件描述符通信 线程间数据传递
信号 通过信号通信 线程间通知

常用且较为基础的有 共享变量、互斥锁、条件变量、信号量(无名)、管道(无名)。

6.1 线程间互斥锁通信

在C语言中,线程间互斥锁(Mutex)是一种用于同步线程的机制,确保多个线程不会同时访问共享资源,从而避免竞态条件(Race Condition)。互斥锁的基本思想是,当一个线程需要访问共享资源时,它会先锁定互斥锁,其他线程在尝试锁定同一个互斥锁时会被阻塞,直到第一个线程释放锁。

6.1.1 . 互斥锁的基本操作

在C语言中,互斥锁通常通过 pthread 库来实现。以下是互斥锁的基本操作:

在使用互斥锁之前,需要先初始化它。可以使用 pthread_mutex_init 函数来初始化互斥锁。

1
2
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

或者使用 pthread_mutex_init 函数:

1
2
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

当一个线程需要访问共享资源时,可以使用 pthread_mutex_lock 函数来锁定互斥锁。如果互斥锁已经被其他线程锁定,当前线程会被阻塞,直到互斥锁被释放。

1
pthread_mutex_lock(&mutex);

当线程完成对共享资源的访问后,应该使用 pthread_mutex_unlock 函数来解锁互斥锁,以便其他线程可以访问共享资源。

1
pthread_mutex_unlock(&mutex);

当互斥锁不再需要时,应该使用 pthread_mutex_destroy 函数来销毁它,释放相关资源。

1
pthread_mutex_destroy(&mutex);

6.1.2 . 示例代码

以下是一个简单的示例,展示了如何使用互斥锁来同步两个线程对共享资源的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <pthread.h>

#include "head.h"

pthread_mutex_t mutex;
int conut = 0;
// 线程函数,不接受参数
void *thread_function1(void *arg) {
while(1)
{
pthread_mutex_lock(&mutex);
conut++;
printf("conut++\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}


// 线程函数,不接受参数
void *thread_function2(void *arg) {
int temp = 0;
while(1)
{


pthread_mutex_lock(&mutex);
if(temp != conut)
{
printf("conut:%d\n",conut);
temp = conut;
}
pthread_mutex_unlock(&mutex);
sleep(1);
}
}

int main() {
pthread_t tid1 = 0;
pthread_t tid2 = 0;
pthread_mutex_init(&mutex,NULL);

// 创建线程,不传递参数
if (pthread_create(&tid1, NULL, thread_function1, NULL) != 0) {
fprintf(stderr, "Failed to create thread\n");
return 1;
}

// 创建线程,不传递参数
if (pthread_create(&tid2, NULL, thread_function2, NULL) != 0) {
fprintf(stderr, "Failed to create thread\n");
return 1;
}

// 等待线程结束
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);


pthread_mutex_destroy(&mutex);

return 0;
}

6.1.3 . 注意事项

  • 死锁:如果多个线程互相等待对方释放锁,可能会导致死锁。因此,在使用互斥锁时,要确保锁的获取和释放顺序一致。
  • 性能:频繁的锁操作可能会影响程序的性能,因此应尽量减少锁的持有时间。

通过使用互斥锁,可以有效地避免多个线程同时访问共享资源导致的数据不一致问题。

6.1.4 如何避免死锁问题

  • 尽量保持线程顺序进行,即保持同步。
  • 避免互斥锁嵌套

6.2 线程间信号量(无名)通信(同步)

无名信号量(Anonymous Semaphore),也称为基于内存的信号量,是一种用于线程间或进程间同步的机制。与有名信号量不同,无名信号量没有关联的文件系统路径名,而是直接存储在内存中。它通常用于同一进程内的线程间同步,但也可以通过共享内存的方式用于进程间同步。

以下是关于无名信号量的详细介绍:


6.2.1 . 无名信号量的特点

  • 无名称:无名信号量没有关联的文件系统路径名,因此只能通过内存地址访问。
  • 高效:由于不需要文件系统操作,无名信号量的性能通常比有名信号量更高。
  • 适用范围
    • 主要用于同一进程内的线程间同步。
    • 如果需要在进程间使用,可以将无名信号量放置在共享内存中。
  • 生命周期:无名信号量的生命周期与创建它的进程或线程绑定,进程退出后信号量会自动销毁。

6.2.2 . 无名信号量的操作函数

无名信号量的操作主要通过以下函数实现:

6.2.2.1 (1)初始化信号量
1
int sem_init(sem_t *sem, int pshared, unsigned int value);
  • 功能:初始化一个无名信号量。
  • 参数
    • sem:指向信号量对象的指针。
    • pshared
      • 0:信号量用于线程间同步。
      • 非零值:信号量用于进程间同步(需要将信号量放置在共享内存中)。
    • value:信号量的初始值。
  • 返回值:成功返回 0,失败返回 -1
6.2.2.2 (2)等待信号量(P 操作)
1
int sem_wait(sem_t *sem);
  • 功能:等待信号量。如果信号量的值大于 0,则减 1 并继续执行;否则,线程阻塞。
  • 参数
    • sem:指向信号量对象的指针。
  • 返回值:成功返回 0,失败返回 -1
6.2.2.3 (3)释放信号量(V 操作)
1
int sem_post(sem_t *sem);
  • 功能:释放信号量,将信号量的值加 1,并唤醒等待的线程。
  • 参数
    • sem:指向信号量对象的指针。
  • 返回值:成功返回 0,失败返回 -1
6.2.2.4 (4)销毁信号量
1
int sem_destroy(sem_t *sem);
  • 功能:销毁一个无名信号量。
  • 参数
    • sem:指向信号量对象的指针。
  • 返回值:成功返回 0,失败返回 -1

6.2.3 . 无名信号量的使用场景

无名信号量通常用于以下场景:

  1. 线程间同步
    • 控制多个线程对共享资源的访问。
    • 实现生产者-消费者模型。
  2. 进程间同步
    • 将无名信号量放置在共享内存中,供多个进程使用。
  3. 二进制信号量
    • 信号量的初始值为 1,用于实现互斥锁的功能。

6.2.4 . 无名信号量与有名信号量的区别

特性 无名信号量 有名信号量
名称 无名称,通过内存地址访问 有名称,通过文件系统路径访问
适用范围 主要用于线程间同步 可用于线程间和进程间同步
性能 更高(无需文件系统操作) 较低(涉及文件系统操作)
生命周期 与创建它的进程或线程绑定 独立于进程,需手动销毁
初始化方式 sem_init sem_open
销毁方式 sem_destroy sem_closesem_unlink

6.2.5 . 无名信号量的示例

以下是一个简单的示例,展示如何使用无名信号量实现线程间同步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

#include "head.h"

sem_t sem;
int conut = 0;
// 线程函数,不接受参数
void *thread_function1(void *arg) {
while(1)
{
sem_post(&sem);
conut++;
sleep(1);
printf("conut++\n");
}
}


// 线程函数,不接受参数
void *thread_function2(void *arg) {
int temp = 0;
while(1)
{
if(temp != conut)
{
sem_wait(&sem);
printf("conut:%d\n",conut);
temp = conut;
}

}
}

int main() {
pthread_t tid1 = 0;
pthread_t tid2 = 0;

// 初始化无名信号量,初始值为 1(二进制信号量)
if (sem_init(&sem, 0, 1) != 0) {
fprintf(stderr, "Failed to initialize semaphore\n");
return 1;
}
// 创建线程,不传递参数
if (pthread_create(&tid1, NULL, thread_function1, NULL) != 0) {
fprintf(stderr, "Failed to create thread\n");
return 1;
}

// 创建线程,不传递参数
if (pthread_create(&tid2, NULL, thread_function2, NULL) != 0) {
fprintf(stderr, "Failed to create thread\n");
return 1;
}

// 等待线程结束
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);

sem_destroy(&sem);


return 0;
}

output:

1
2
3
4
5
6
7
8
conut:1
conut++
conut:2
conut++
conut:3
conut++
conut:4
conut++

无名信号量存在sem_wait(&sem);阻塞等待的问题,所以在多个线程时使用互斥锁并与条件变量配合使用。

6.3 线程间条件变量通信(同步)

6.3.1 条件变量

条件变量(Condition Variable)是一种线程同步机制,通常与互斥锁(Mutex)结合使用,用于实现线程间的复杂同步。它的核心功能是允许线程在某个条件不满足时进入休眠状态,并在条件满足时被唤醒。条件变量是多线程编程中非常重要的工具,常用于实现生产者-消费者模型、线程池等场景。


6.3.2 条件变量的核心概念

  • 等待条件:线程可以检查某个条件是否满足,如果不满足,则进入休眠状态。
  • 通知条件:当条件满足时,其他线程可以唤醒等待的线程。
  • 与互斥锁结合:条件变量通常与互斥锁一起使用,以确保对共享资源的访问是线程安全的。

6.3.3 条件变量的操作函数

条件变量的操作主要通过以下函数实现:

6.3.3.1 初始化条件变量
1
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
  • 功能:初始化一个条件变量。
  • 参数
    • cond:指向条件变量对象的指针。
    • attr:条件变量的属性,通常设置为 NULL,表示使用默认属性。
  • 返回值:成功返回 0,失败返回错误码。
6.3.3.2 销毁条件变量
1
int pthread_cond_destroy(pthread_cond_t *cond);
  • 功能:销毁一个条件变量。
  • 参数
    • cond:指向条件变量对象的指针。
  • 返回值:成功返回 0,失败返回错误码。
6.3.3.3 等待条件变量
1
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • 功能:等待条件变量。线程会释放互斥锁并进入休眠状态,直到被唤醒。
  • 参数
    • cond:指向条件变量对象的指针。
    • mutex:指向互斥锁对象的指针。
  • 返回值:成功返回 0,失败返回错误码。
6.3.3.4 唤醒一个等待的线程
1
int pthread_cond_signal(pthread_cond_t *cond);
  • 功能:唤醒一个等待条件变量的线程。
  • 参数
    • cond:指向条件变量对象的指针。
  • 返回值:成功返回 0,失败返回错误码。
6.3.3.5 唤醒所有等待的线程
1
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 功能:唤醒所有等待条件变量的线程。
  • 参数
    • cond:指向条件变量对象的指针。
  • 返回值:成功返回 0,失败返回错误码。

6.3.3.6 条件变量的使用模式

条件变量通常与互斥锁和共享变量一起使用,以下是一个典型的使用模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pthread_mutex_t mutex;
pthread_cond_t cond;
int condition = 0;

// 线程 1:等待条件
void *thread1(void *arg) {
pthread_mutex_lock(&mutex);
while (condition == 0) { // 检查条件
pthread_cond_wait(&cond, &mutex); // 等待条件变量
}
// 条件满足,执行操作
pthread_mutex_unlock(&mutex);
return NULL;
}

// 线程 2:设置条件
void *thread2(void *arg) {
pthread_mutex_lock(&mutex);
condition = 1; // 修改条件
pthread_cond_signal(&cond); // 唤醒等待的线程
pthread_mutex_unlock(&mutex);
return NULL;
}

6.3.3.7 条件变量的注意事项
  1. 虚假唤醒

    • 线程可能会在没有收到通知的情况下被唤醒,因此需要在 pthread_cond_wait 返回后重新检查条件。

    • 通常将 pthread_cond_wait 放在 while 循环中:

      1
      2
      3
      while (condition == 0) {
      pthread_cond_wait(&cond, &mutex);
      }
  2. 与互斥锁的配合

    • 在调用 pthread_cond_wait 之前,必须持有互斥锁。
    • pthread_cond_wait 会释放互斥锁并进入休眠状态,被唤醒后会重新获取互斥锁。
  3. 避免死锁

    • 确保在修改条件和调用 pthread_cond_signalpthread_cond_broadcast 时持有互斥锁。
    • 确保在等待条件变量时正确释放和重新获取互斥锁。

6.3.4 示例代码

以下是一个完整的生产者-消费者模型的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int count = 0; // 缓冲区中的元素数量

pthread_mutex_t mutex;
pthread_cond_t cond_producer;
pthread_cond_t cond_consumer;

// 生产者线程
void *producer(void *arg) {
for (int i = 0; i < 20; i++) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) { // 缓冲区满,等待
pthread_cond_wait(&cond_producer, &mutex);
}
buffer[count++] = i; // 生产数据
printf("Produced: %d\n", i);
pthread_cond_signal(&cond_consumer); // 唤醒消费者
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}

// 消费者线程
void *consumer(void *arg) {
for (int i = 0; i < 20; i++) {
pthread_mutex_lock(&mutex);
while (count == 0) { // 缓冲区空,等待
pthread_cond_wait(&cond_consumer, &mutex);
}
int item = buffer[--count]; // 消费数据
printf("Consumed: %d\n", item);
pthread_cond_signal(&cond_producer); // 唤醒生产者
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}

int main() {
pthread_t tid_producer, tid_consumer;

// 初始化互斥锁和条件变量
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_producer, NULL);
pthread_cond_init(&cond_consumer, NULL);

// 创建生产者和消费者线程
pthread_create(&tid_producer, NULL, producer, NULL);
pthread_create(&tid_consumer, NULL, consumer, NULL);

// 等待线程结束
pthread_join(tid_producer, NULL);
pthread_join(tid_consumer, NULL);

// 销毁互斥锁和条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_producer);
pthread_cond_destroy(&cond_consumer);

return 0;
}

6.4 线程间管道(无名)通信

管道是一种半双工的通信机制,数据只能单向流动。它有两个端点:

  • 写端:用于写入数据。
  • 读端:用于读取数据。

在 Linux 中,管道通过 pipe() 系统调用创建,返回两个文件描述符:一个用于读,一个用于写。


6.4.1 线程间使用管道通信的步骤

以下是在同一进程内的线程间使用管道通信的基本步骤:

使用 pipe() 函数创建管道:

1
2
3
4
5
6
7
#include <unistd.h>

int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}

  • pipefd[0] 是读端文件描述符。
  • pipefd[1] 是写端文件描述符。

使用 pthread_create() 创建线程,并将管道文件描述符传递给线程函数。

  • 一个线程通过 write() 向管道写入数据。
  • 另一个线程通过 read() 从管道读取数据。

通信完成后,使用 close() 关闭管道文件描述符。

6.4.2 测验

演示两个线程如何通过管道通信:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

void* writer_thread(void* arg) {
int pipefd = *(int*)arg;
const char* message = "Hello from writer thread!";
write(pipefd, message, strlen(message) + 1); // 写入数据
close(pipefd); // 关闭写端
return NULL;
}

void* reader_thread(void* arg) {
int pipefd = *(int*)arg;
char buffer[100];
read(pipefd, buffer, sizeof(buffer)); // 读取数据
printf("Reader thread received: %s\n", buffer);
close(pipefd); // 关闭读端
return NULL;
}

int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}

pthread_t writer, reader;
pthread_create(&writer, NULL, writer_thread, &pipefd[1]); // 创建写线程
pthread_create(&reader, NULL, reader_thread, &pipefd[0]); // 创建读线程

pthread_join(writer, NULL); // 等待写线程结束
pthread_join(reader, NULL); // 等待读线程结束

return 0;
}

7 主要的进程间通信方法

进程间通信:

通信方式 描述 适用场景
管道 半双工,字节流 父子进程间通信
命名管道 通过文件系统路径访问 无关进程间通信
消息队列 消息传递,支持消息边界 进程间结构化数据传递
共享内存 共享内存区域,速度快 高性能数据共享
信号量 进程间同步 资源计数或互斥
信号 异步通知 事件通知
套接字 支持本地和网络通信 跨机器或本地进程间通信

对于无名管道的操作与线程类似,但对于进程间通信无名方式只适用于有亲缘关系的进程。

7.1 命名管道

有名管道(Named Pipe),也称为 FIFO(First In First Out),是一种特殊的文件类型,用于实现进程间通信(IPC)。与匿名管道(Pipe)不同,有名管道可以在无关的进程之间使用,因为它通过文件系统中的路径名来标识。


7.1.1 . 有名管道的特点

  • 基于文件系统:有名管道在文件系统中有一个路径名,多个进程可以通过该路径名访问管道。
  • 半双工通信:数据只能单向流动,如果需要双向通信,需要创建两个有名管道。
  • 阻塞与非阻塞:默认情况下,读操作和写操作是阻塞的,但可以通过设置非阻塞模式来改变行为。
  • 持久性:有名管道在文件系统中存在,直到被显式删除。

7.1.2 . 有名管道的操作步骤

7.1.2.1 创建有名管道

使用 mkfifo 命令或 mkfifo 函数创建有名管道。

使用 mkfifo 命令:

1
mkfifo /tmp/myfifo

使用 mkfifo 函数:

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname:有名管道的路径名。
  • mode:管道的权限模式(如 0666)。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
if (mkfifo("/tmp/myfifo", 0666) == -1) {
perror("mkfifo");
exit(1);
}
printf("FIFO created at /tmp/myfifo\n");
return 0;
}

7.1.2.2 打开有名管道

使用 open 函数打开有名管道。打开时需要指定读写模式:

  • 只读模式O_RDONLY
  • 只写模式O_WRONLY

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd = open("/tmp/myfifo", O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}
printf("FIFO opened for writing\n");
return 0;
}

7.1.2.3 读写有名管道

使用 readwrite 函数进行读写操作。

写入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
int fd = open("/tmp/myfifo", O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}

char *message = "Hello, FIFO!";
write(fd, message, strlen(message) + 1);
close(fd);
return 0;
}

读取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
int fd = open("/tmp/myfifo", O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}

char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("Received message: %s\n", buffer);
close(fd);
return 0;
}
7.1.2.4 关闭和删除有名管道

使用 close 函数关闭管道,使用 unlink 函数删除管道文件。

示例:

1
2
3
#include <unistd.h>

unlink("/tmp/myfifo");


7.1.3 . 完整示例

以下是一个完整的示例,展示如何使用有名管道实现进程间通信。

7.1.3.1 写入端(writer.c):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
// 创建有名管道
if (mkfifo("/tmp/myfifo", 0666) == -1) {
perror("mkfifo");
exit(1);
}

// 打开管道
int fd = open("/tmp/myfifo", O_WRONLY);
if (fd == -1) {
perror("open");
exit(1);
}

// 写入数据
char *message = "Hello, FIFO!";
write(fd, message, strlen(message) + 1);
close(fd);

// 删除管道
unlink("/tmp/myfifo");
return 0;
}
7.1.3.2 读取端(reader.c):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
// 打开管道
int fd = open("/tmp/myfifo", O_RDONLY);
if (fd == -1) {
perror("open");
exit(1);
}

// 读取数据
char buffer[100];
read(fd, buffer, sizeof(buffer));
printf("Received message: %s\n", buffer);
close(fd);
return 0;
}

7.1.4 . 注意事项

  1. 阻塞行为

    • 默认情况下,读操作会阻塞,直到有数据写入;写操作会阻塞,直到有进程读取数据。
    • 可以通过设置非阻塞模式(O_NONBLOCK)来改变行为。
  2. 双向通信

    • 如果需要双向通信,可以创建两个有名管道,一个用于进程 A 到进程 B 的通信,另一个用于进程 B 到进程 A 的通信。
  3. 管道清理

    • 使用 unlink 删除管道文件,避免残留。

7.2 信号

在操作系统中,进程间的信号(Signal)通信是一种异步的通信机制,用于通知进程发生了某种事件。信号可以由操作系统、其他进程或进程自身发送。进程可以捕获信号并执行相应的处理函数,也可以忽略信号或使用默认行为。


7.2.1 . 信号的基本概念

  • 信号是什么
    信号是一个整数,表示某种事件的发生。例如:

    • SIGINT:中断信号(通常由 Ctrl+C 触发)。
    • SIGTERM:终止信号(请求进程终止)。
    • SIGKILL:强制终止信号(无法被捕获或忽略)。
    • SIGUSR1SIGUSR2:用户自定义信号。
  • 信号的来源

    • 由操作系统内核发送(如段错误 SIGSEGV)。
    • 由其他进程发送(通过 kill 系统调用)。
    • 由进程自身发送(通过 raisekill)。
  • 信号的处理方式

    • 捕获信号:进程可以注册一个信号处理函数来处理信号。
    • 忽略信号:进程可以选择忽略某些信号。
    • 默认行为:如果不捕获或忽略信号,进程会执行信号的默认行为(如终止、暂停等)。

    常用的信号

    | 信号名称 | 信号编号 | 类型 | 描述 |
    | :———- | :———- | :—- | :———————————————————————— |
    | SIGHUP | 1 | Term | 控制终端挂起或控制进程死亡。 |
    | SIGINT | 2 | Term | 键盘中断(通常是 Ctrl+C)。 |
    | SIGQUIT | 3 | Core | 键盘退出(通常是 Ctrl+\),并生成核心转储文件。 |
    | SIGILL | 4 | Core | 非法指令。 |
    | SIGABRT | 6 | Core | 由 abort(3) 函数发出的中止信号。 |
    | SIGFPE | 8 | Core | 浮点异常。 |
    | SIGKILL | 9 | Term | 强制终止信号(无法被捕获或忽略)。 |
    | SIGSEGV | 11 | Core | 无效内存引用(段错误)。 |
    | SIGPIPE | 13 | Term | 管道破裂:向没有读取者的管道写入数据。 |
    | SIGALRM | 14 | Term | 由 alarm(2) 函数设置的定时器信号。 |
    | SIGTERM | 15 | Term | 终止信号(请求进程正常终止)。 |
    | SIGUSR1 | 30,10,16 | Term | 用户自定义信号 1。 |
    | SIGUSR2 | 31,12,17 | Term | 用户自定义信号 2。 |
    | SIGCHLD | 20,17,18 | Ign | 子进程停止或终止。 |
    | SIGCONT | 19,18,25 | Cont | 如果进程已停止,则继续执行。 |
    | SIGSTOP | 17,19,23 | Stop | 停止进程(无法被捕获或忽略)。 |
    | SIGTSTP | 18,20,24 | Stop | 终端发出的停止信号(通常是 Ctrl+Z)。 |
    | SIGTTIN | 21,21,26 | Stop | 后台进程尝试从终端读取输入。 |
    | SIGTTOU | 22,22,27 | Stop | 后台进程尝试向终端写入输出。 |

    • 类型
      • Term:终止进程。
      • Core:终止进程并生成核心转储文件。
      • Ign:忽略信号。
      • Cont:继续已停止的进程。
      • Stop:停止进程。
    • 信号编号:不同系统可能使用不同的编号,因此列出了多个可能的编号。

7.2.2 . 信号的发送与接收

7.2.2.1 发送信号

可以使用 kill 系统调用向指定进程发送信号。

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);
  • pid:目标进程的进程 ID。
    • 如果 pid > 0,信号发送给指定进程。
    • 如果 pid == 0,信号发送给与当前进程同组的所有进程。
    • 如果 pid == -1,信号发送给所有有权限发送的进程。
  • sig:要发送的信号(如 SIGUSR1)。

示例:

1
kill(1234, SIGUSR1);  // 向进程 ID 为 1234 的进程发送 SIGUSR1 信号

7.2.2.2 捕获信号

可以使用 signalsigaction 函数来注册信号处理函数。

  • signal 函数

    1
    2
    3
    #include <signal.h>

    void (*signal(int sig, void (*handler)(int)))(int);
    • sig:要捕获的信号。
    • handler:信号处理函数。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>

    void handle_signal(int sig) {
    printf("Received signal: %d\n", sig);
    }

    int main() {
    signal(SIGUSR1, handle_signal); // 捕获 SIGUSR1 信号
    while (1) {
    sleep(1); // 等待信号
    }
    return 0;
    }

​ 示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int flag = 1;
void handler(int sig)
{
flag = 0;
}

int main()
{
int conut = 0;
while(1)
{
signal(SIGUSR1,handler);
if(flag == 1)
{
conut++;
}else
{
conut--;
}
printf("conut:%d\n",conut);
sleep(1);
}
}
  • sigaction 函数
    sigaction 是更强大的信号处理函数,可以设置更多的选项。

    1
    2
    3
    #include <signal.h>

    int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
    • sig:要捕获的信号。
    • act:新的信号处理配置。
    • oldact:旧的信号处理配置(可以为 NULL)。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>

    void handle_signal(int sig) {
    printf("Received signal: %d\n", sig);
    }

    int main() {
    struct sigaction sa;
    sa.sa_handler = handle_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    sigaction(SIGUSR1, &sa, NULL); // 捕获 SIGUSR1 信号
    while (1) {
    sleep(1); // 等待信号
    }
    return 0;
    }

7.2.3 . 信号的默认行为

如果不捕获信号,进程会执行信号的默认行为。常见的默认行为包括:

  • 终止进程:如 SIGTERMSIGINT
  • 忽略信号:如 SIGCHLD
  • 暂停进程:如 SIGSTOP
  • 终止并生成核心转储文件:如 SIGSEGV

7.2.4 . 信号的阻塞与解除阻塞

进程可以阻塞某些信号,使其暂时不被处理。可以使用 sigprocmask 函数来设置信号掩码。

1
2
3
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how:操作类型,如 SIG_BLOCK(阻塞信号)、SIG_UNBLOCK(解除阻塞)、SIG_SETMASK(设置新的信号掩码)。
  • set:要操作的信号集。
  • oldset:旧的信号集(可以为 NULL)。

示例:

1
2
3
4
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGUSR1); // 将 SIGUSR1 添加到信号集
sigprocmask(SIG_BLOCK, &set, NULL); // 阻塞 SIGUSR1


7.2.5 . 示例:进程间信号通信

以下是一个简单的示例,展示了一个进程向另一个进程发送信号并捕获信号的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void handle_signal(int sig) {
printf("Child process received signal: %d\n", sig);
}

int main() {
pid_t pid = fork();

if (pid == 0) { // 子进程
signal(SIGUSR1, handle_signal); // 捕获 SIGUSR1
while (1) {
sleep(1); // 等待信号
}
} else if (pid > 0) { // 父进程
sleep(2); // 等待子进程准备好
printf("Parent sending SIGUSR1 to child\n");
kill(pid, SIGUSR1); // 向子进程发送 SIGUSR1
sleep(1); // 等待子进程处理信号
kill(pid, SIGTERM); // 终止子进程
} else {
perror("fork failed");
exit(1);
}

return 0;
}

7.2.6 . 注意事项

  • 信号是异步的:信号可以在任何时候中断进程的执行。
  • 信号处理函数应尽量简单:避免在信号处理函数中执行复杂的操作。
  • 信号可能丢失:如果多个相同的信号在短时间内发送,进程可能只会收到一个信号。
  • 不可靠信号与可靠信号:早期的 UNIX 信号机制是不可靠的,现代系统(如 Linux)提供了可靠的信号机制(如 sigaction)。

7.3 IPC与IPCS命令

IPC 是操作系统提供的一种机制,用于在不同进程之间传递数据或同步操作。

  1. 同步通信(Synchronous Communication)

同步通信是指通信的双方(发送者和接收者)必须协调时序,发送者发送数据后,必须等待接收者接收并处理数据,才能继续执行后续操作。

  1. 异步通信(Asynchronous Communication)

异步通信是指通信的双方不需要协调时序,发送者发送数据后,可以立即继续执行后续操作,而不需要等待接收者接收数据。

7.3.1 ipcs 命令

ipcs 是一个用于查看系统中 IPC 资源状态的命令行工具。它可以显示当前系统中使用的消息队列、共享内存段和信号量集的信息。

7.3.1.1 . 命令格式
1
ipcs [选项]
7.3.1.2 . 常用选项
选项 描述
-a 显示所有 IPC 资源(默认选项)。
-q 仅显示消息队列(Message Queue)信息。
-m 仅显示共享内存(Shared Memory)信息。
-s 仅显示信号量(Semaphore)信息。
-l 显示 IPC 资源的系统限制(如最大消息队列数、最大共享内存大小等)。
-p 显示与 IPC 资源相关的进程 ID(创建者和最后操作者)。
-t 显示 IPC 资源的访问时间(最后操作时间)。
-c 显示 IPC 资源的创建者和所有者的用户 ID 和组 ID。
-u 显示 IPC 资源的使用情况摘要。
7.3.1.3 . 输出字段说明
  • 消息队列(Message Queue)

    • msqid:消息队列的唯一标识符。
    • owner:消息队列的所有者。
    • perms:权限(访问控制)。
    • used-bytes:消息队列中已使用的字节数。
    • messages:消息队列中的消息数量。
  • 共享内存(Shared Memory)

    • shmid:共享内存段的唯一标识符。
    • owner:共享内存段的所有者。
    • perms:权限(访问控制)。
    • bytes:共享内存段的大小。
    • nattch:附加到共享内存段的进程数量。
  • 信号量(Semaphore)

    • semid:信号量集的唯一标识符。
    • owner:信号量集的所有者。
    • perms:权限(访问控制)。
    • nsems:信号量集中的信号量数量。
  • ```
    ——— Message Queues ————
    key msqid owner perms used-bytes messages
    0x41021208 0 serenitati 666 0 0

    ——— Shared Memory Segments ————
    key shmid owner perms bytes nattch status

    ——— Semaphore Arrays ————
    key semid owner perms nsems

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60



    #### `ipcrm` 命令

    `ipcrm` 是用于删除 IPC 资源的命令。可以通过指定 IPC 资源的 ID 或键值来删除消息队列、共享内存段或信号量集。

    #### 2. 常用选项

    | 选项 | 描述 |
    | :--- | :----------------------- |
    | `-q` | 删除消息队列。 |
    | `-m` | 删除共享内存段。 |
    | `-s` | 删除信号量集。 |
    | `-Q` | 通过键值删除消息队列。 |
    | `-M` | 通过键值删除共享内存段。 |
    | `-S` | 通过键值删除信号量集。 |

    ### 消息队列

    消息队列(Message Queue)是一种进程间通信(IPC)机制,允许不同进程通过发送和接收消息来交换数据。消息队列的核心思想是:消息的发送者和接收者通过一个共享的队列进行通信,发送者将消息放入队列,接收者从队列中取出消息。

    消息队列的特点是:
    - **异步通信**:发送者和接收者不需要同时运行。
    - **消息边界**:消息是独立的数据单元,接收者可以一次读取一条完整的消息。
    - **优先级支持**:可以为消息设置优先级,高优先级的消息先被处理。

    ---

    #### 1. 消息队列的基本概念

    - **消息**:消息队列中的数据单元,通常是一个结构体,包含消息类型和消息内容。
    - **消息队列标识符**:每个消息队列都有一个唯一的标识符(`msqid`),用于标识消息队列。
    - **消息类型**:每条消息都有一个类型字段,接收者可以根据类型选择性地读取消息。

    ---

    #### 2. 消息队列的操作函数

    在 Linux 中,消息队列的操作主要通过以下系统调用实现:

    | 函数 | 描述 |
    | -------- | ---------------------------------------------------- |
    | `msgget` | 创建或获取一个消息队列。 |
    | `msgsnd` | 向消息队列发送一条消息。 |
    | `msgrcv` | 从消息队列接收一条消息。 |
    | `msgctl` | 控制消息队列(如删除消息队列、获取消息队列信息等)。 |

    ---

    #### 3. 消息队列的使用步骤

    ##### 3.1 创建或获取消息队列

    使用 `msgget` 函数创建或获取一个消息队列。

    ```c
    #include <sys/msg.h>

    int msgget(key_t key, int msgflg);
  • key:消息队列的键值,通常使用 ftok 函数生成。

  • msgflg:标志位,如 IPC_CREAT(创建消息队列)和权限模式(如 0666)。

示例:

1
2
key_t key = ftok("/tmp", 'A');  // 生成键值
int msqid = msgget(key, 0666 | IPC_CREAT); // 创建或获取消息队列

3.2 发送消息

使用 msgsnd 函数向消息队列发送消息。

1
2
3
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid:消息队列标识符。
  • msgp:指向消息的指针,消息结构体必须包含一个 long 类型的消息类型字段。
  • msgsz:消息的大小(不包括消息类型字段)。
  • msgflg:标志位,如 IPC_NOWAIT(非阻塞发送)。

消息结构体示例:

1
2
3
4
struct msgbuf {
long mtype; // 消息类型
char mtext[100]; // 消息内容
};

发送消息示例:

1
2
3
4
5
struct msgbuf msg;
msg.mtype = 1; // 设置消息类型
strcpy(msg.mtext, "Hello, Message Queue!");

msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 发送消息

3.3 接收消息

使用 msgrcv 函数从消息队列接收消息。

1
2
3
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • msqid:消息队列标识符。
  • msgp:指向消息的指针。
  • msgsz:消息缓冲区的大小。
  • msgtyp:消息类型:
    • 0:接收队列中的第一条消息。
    • > 0:接收指定类型的消息。
    • < 0:接收类型小于或等于 |msgtyp| 的第一条消息。
  • msgflg:标志位,如 IPC_NOWAIT(非阻塞接收)。

接收消息示例:

1
2
3
struct msgbuf msg;
msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0); // 接收类型为 1 的消息
printf("Received message: %s\n", msg.mtext);

3.4 控制消息队列

使用 msgctl 函数控制消息队列。

1
2
3
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • msqid:消息队列标识符。
  • cmd:控制命令,如 IPC_RMID(删除消息队列)。
  • buf:指向 msqid_ds 结构的指针,用于获取或设置消息队列信息。

删除消息队列示例:

1
msgctl(msqid, IPC_RMID, NULL);  // 删除消息队列

4. 完整示例

以下是一个完整的示例,展示如何使用消息队列进行进程间通信。

发送者程序(sender.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>

struct msgbuf {
long mtype;
char mtext[100];
};

int main() {
key_t key = ftok("/tmp", 'A');
int msqid = msgget(key, 0666 | IPC_CREAT);

struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "Hello, Message Queue!");

msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
printf("Message sent: %s\n", msg.mtext);

return 0;
}

接收者程序(receiver.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/msg.h>

struct msgbuf {
long mtype;
char mtext[100];
};

int main() {
key_t key = ftok("/tmp", 'A');
int msqid = msgget(key, 0666 | IPC_CREAT);

struct msgbuf msg;
msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
printf("Message received: %s\n", msg.mtext);

msgctl(msqid, IPC_RMID, NULL); // 删除消息队列
return 0;
}

编译并运行:

1
2
3
4
5
gcc -o sender sender.c
gcc -o receiver receiver.c

./sender
./receiver

共享内存

共享内存(Shared Memory)是一种高效的进程间通信(IPC)机制,允许多个进程共享同一块内存区域。由于数据直接存储在内存中,共享内存的通信速度比其他 IPC 机制(如管道、消息队列)更快。然而,共享内存本身不提供同步机制,因此通常需要结合信号量或互斥锁来避免竞态条件。


1. 共享内存的特点

  • 高效:数据直接存储在内存中,无需内核介入。
  • 无同步机制:共享内存本身不提供进程间的同步,需要额外的同步机制(如信号量)。
  • 共享性:多个进程可以同时访问同一块内存区域。
  • 持久性:共享内存段会一直存在,直到显式删除或系统重启。

2. 共享内存的操作步骤

2.1 创建共享内存段

使用 shmget 函数创建共享内存段。

1
2
3
4
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • key:共享内存段的键值,通常使用 ftok 生成。
  • size:共享内存段的大小(字节)。
  • shmflg:标志位,如 IPC_CREAT(创建共享内存段)和权限模式(如 0666)。

示例:

1
2
key_t key = ftok("/tmp", 'A');
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);

2.2 附加共享内存段

使用 shmat 函数将共享内存段附加到进程的地址空间。

1
2
3
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:共享内存段的标识符。
  • shmaddr:指定附加地址(通常为 NULL,由系统选择)。
  • shmflg:标志位,如 SHM_RDONLY(只读模式)。

示例:

1
char *shmaddr = (char *)shmat(shmid, NULL, 0);

2.3 使用共享内存

进程可以通过附加的共享内存地址直接读写数据。

示例:

1
strcpy(shmaddr, "Hello, Shared Memory!");

2.4 分离共享内存段

使用 shmdt 函数将共享内存段从进程的地址空间分离。

1
2
3
#include <sys/shm.h>

int shmdt(const void *shmaddr);
  • shmaddr:共享内存段的附加地址。

示例:

1
shmdt(shmaddr);

2.5 删除共享内存段

使用 shmctl 函数删除共享内存段。

1
2
3
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享内存段的标识符。
  • cmd:控制命令,如 IPC_RMID(删除共享内存段)。
  • buf:指向 shmid_ds 结构的指针(可以为 NULL)。

示例:

1
shmctl(shmid, IPC_RMID, NULL);


3. 完整示例

以下是一个完整的示例,展示如何使用共享内存实现进程间通信。

写入端(writer.c):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main() {
// 生成键值
key_t key = ftok("/tmp", 'A');
if (key == -1) {
perror("ftok");
exit(1);
}

// 创建共享内存段
int shmid = shmget(key, 1024, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(1);
}

// 附加共享内存段
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat");
exit(1);
}

// 写入数据
strcpy(shmaddr, "Hello, Shared Memory!");
printf("Data written to shared memory: %s\n", shmaddr);

// 分离共享内存段
shmdt(shmaddr);
return 0;
}
读取端(reader.c):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main() {
// 生成键值
key_t key = ftok("/tmp", 'A');
if (key == -1) {
perror("ftok");
exit(1);
}

// 获取共享内存段
int shmid = shmget(key, 1024, 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}

// 附加共享内存段
char *shmaddr = (char *)shmat(shmid, NULL, 0);
if (shmaddr == (char *)-1) {
perror("shmat");
exit(1);
}

// 读取数据
printf("Data read from shared memory: %s\n", shmaddr);

// 分离共享内存段
shmdt(shmaddr);

// 删除共享内存段
shmctl(shmid, IPC_RMID, NULL);
return 0;
}

有名型号量

​ 有名信号量(Named Semaphore)是一种用于进程间同步的机制,它通过文件系统中的名字来标识信号量,因此可以用于无关进程间的同步。有名信号量的生命周期独立于创建它的进程,直到显式删除。

有名信号量与无名信号量间的区别

作用范围:

特性 无名信号量 有名信号量
作用范围 线程间或父子进程间 无关进程间
共享方式 基于内存,通常位于共享内存中 基于文件系统,通过名字标识

生命周期:

特性 无名信号量 有名信号量
生命周期 与创建它的进程或线程绑定 独立于创建它的进程
持久性 无持久性,进程退出后信号量被销毁 有持久性,信号量会一直存在,直到显式删除

使用场景:

特性 无名信号量 有名信号量
适用场景 线程间同步、父子进程间同步 无关进程间同步
实现复杂度 较简单 较复杂

创建和销毁方式:

特性 无名信号量 有名信号量
创建方式 使用 sem_init 初始化 使用 sem_open 创建或打开
销毁方式 使用 sem_destroy 销毁 使用 sem_close 关闭和 sem_unlink 删除

1. 有名信号量的特点

  • 基于文件系统:有名信号量通过文件系统中的名字标识,可以在无关进程间共享。
  • 持久性:有名信号量会一直存在,直到显式删除(使用 sem_unlink)。
  • 同步机制:有名信号量提供了一种简单的同步机制,可以用于控制多个进程对共享资源的访问。

2. 有名信号量的操作函数

有名信号量的操作函数定义在 <semaphore.h> 头文件中,主要包括以下函数:

函数 描述
sem_open 创建或打开一个有名信号量。
sem_wait 等待信号量(信号量值减 1)。
sem_post 释放信号量(信号量值加 1)。
sem_trywait 非阻塞等待信号量。
sem_timedwait 带超时的等待信号量。
sem_close 关闭有名信号量。
sem_unlink 删除有名信号量。

3. 有名信号量的使用步骤

3.1 创建或打开有名信号量

使用 sem_open 函数创建或打开一个有名信号量。

1
2
3
#include <semaphore.h>

sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
  • name:信号量的名字,通常以 / 开头(如 /mysem)。
  • oflag:标志位,如 O_CREAT(创建信号量)和 O_EXCL(如果信号量已存在则失败)。
  • mode:权限模式(如 0666)。
  • value:信号量的初始值。

示例:

1
2
3
4
5
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}

3.2 等待信号量

使用 sem_wait 函数等待信号量(信号量值减 1)。

1
2
3
#include <semaphore.h>

int sem_wait(sem_t *sem);
  • sem:指向信号量的指针。

示例:

1
sem_wait(sem);  // 等待信号量

3.3 释放信号量

使用 sem_post 函数释放信号量(信号量值加 1)。

1
2
3
#include <semaphore.h>

int sem_post(sem_t *sem);
  • sem:指向信号量的指针。

示例:

1
sem_post(sem);  // 释放信号量

3.4 关闭有名信号量

使用 sem_close 函数关闭有名信号量。

1
2
3
#include <semaphore.h>

int sem_close(sem_t *sem);
  • sem:指向信号量的指针。

示例:

1
sem_close(sem);  // 关闭信号量

3.5 删除有名信号量

使用 sem_unlink 函数删除有名信号量。

1
2
3
#include <semaphore.h>

int sem_unlink(const char *name);
  • name:信号量的名字。

示例:

1
sem_unlink("/mysem");  // 删除信号量


4. 完整示例

以下是一个完整的示例,展示如何使用有名信号量实现进程间同步。

写入端(writer.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// 创建或打开有名信号量
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}

// 等待信号量
printf("Writer waiting for semaphore...\n");
sem_wait(sem);
printf("Writer acquired semaphore!\n");

// 模拟工作
sleep(2);

// 释放信号量
printf("Writer releasing semaphore...\n");
sem_post(sem);

// 关闭信号量
sem_close(sem);

// 删除信号量
sem_unlink("/mysem");

return 0;
}

读取端(reader.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// 打开有名信号量
sem_t *sem = sem_open("/mysem", 0);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}

// 等待信号量
printf("Reader waiting for semaphore...\n");
sem_wait(sem);
printf("Reader acquired semaphore!\n");

// 模拟工作
sleep(2);

// 释放信号量
printf("Reader releasing semaphore...\n");
sem_post(sem);

// 关闭信号量
sem_close(sem);

return 0;
}