进程的概念及基础

程序和进程的关系

程序:是静态的,一堆没有被加载(运行)的二进制代码,存储在硬盘

进程:是动态的,已经被加载(运行)到内存中的程序文件,存储到内存

进程的相关命令

查看:Linux 基础与实战笔记-进程与服务管理

进程的生命周期状态(重点)img

1、就绪态:当一个程序被加载后就处于就绪态中,所有的进程要变成执行态,都要从就绪态开始

2、执行态:当一个进程得到 CPU 的使用权(时间片)后,就会处于执行态中

3、睡眠/挂起态:当一个进程调用 sleep 或者一些等待阻塞函数 scanf 时,都会进入到睡眠/挂起态

4、暂停态:收到 STOP 暂停信号后就会进入到暂停态中,等待 CONT 继续信号

5、僵尸态:进程死亡后都会进入到僵尸态,等待别人帮他收尸

6、死亡态:父母进程回收(wait)子进程的资源就会进入死亡态

进程的相关 API 和说明

进程的创建(fork 函数)

父子进程在 fork() 之后的区别,核心就一条:

👉 父进程和子进程的虚拟内存内容相同,但属于不同的进程,PID 不同,资源不共享(文件描述符除外)

具体来说:

  1. 不同点
    • PID 不同getpid() 获取到的进程号不同。
    • fork() 的返回值不同
      • 父进程中 fork() 返回子进程 PID。
      • 子进程中 fork() 返回 0
    • 资源独立:全局变量、局部变量等虽然初始值相同,但各自独立修改,互不影响
  2. 相同点
    • 代码段相同:执行同一段程序。
    • 数据段、堆、栈初始内容相同(fork 后复制)
    • 打开的文件描述符相同(共享文件偏移量)

父子进程共享同一份程序代码,调度由操作系统时间片复用完成,所以运行起来像同一个程序

它们的区别只能通过 fork() 的返回值进程号(PID) 来区分

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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[]) {
int value = 100;
// 获取本进程的id
printf("fork之前:pid==%d\n", getpid());

// fork后有两个进程
pid_t pid = fork();
if (pid < 0) {
perror("fork error!\n");
return -1;
} else if (pid == 0) { // 字进程
value = 110;
printf("fork之后,pid==%d,&value:%p,value:%d\n", getpid(), &value, value);
return 0;
} else { // 父进程
value = 120;
printf("fork之后,pid==%d,&value:%p,value:%d\n", getpid(), &value, value);
return 0;
}
return 0;
}

运行结果如下:

1
2
3
4
 ➜ ./1.fork函数创建进程
fork之前:pid==977163
fork之后,pid==977163,&value:0x7fffde4f16c0,value:120
fork之后,pid==977164,&value:0x7fffde4f16c0,value:110

可以看到,父进程和字进程变量的虚拟内存地址相同,但值不同

进程的回收(wait, waitpid, 状态值)

当一个子进程退出时,它会进入僵尸态。此时,子进程的资源(如内存)大部分已被释放,但在内核的进程表中仍然保留其退出状态等信息。父进程需要调用 wait()waitpid() 来获取这些信息,并彻底清理子进程的记录,这个过程称为“为子进程收尸”。

1. wait() 函数

wait() 函数会阻塞父进程,直到它的任意一个子进程结束。

  • 函数原型pid_t wait(int *wstatus);
  • 参数wstatus 是一个整型指针,用于接收子进程的退出状态。如果不需要,可以传入 NULL
  • 返回值:成功时返回结束的子进程的 PID;失败时返回 -1。

下面的代码演示了父进程如何使用 wait() 等待子进程结束。子进程运行 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 <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char const *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork error!\n");
return -1;
} else if (pid == 0) { // 子进程
int i = 0;
while (1) {
printf("pid(%d): play game(%d)......\n", getpid(), i);
sleep(1);
i++;
if (i == 5) {
printf("子进程结束!\n");
return 0; // 结束子进程
}
}
} else { // 父进程
printf("父进程正在等待子进程结束!\n");
int wait_status = 0;
// 堵塞于此,等待任意一个子进程结束(并给子进程收尸)
pid_t child_pid = wait(&wait_status);
printf("回收成功,被终止的子进程ID == %d\n", child_pid);
}
return 0;
}
2. waitpid() 函数

waitpid()wait() 的一个更强大、更灵活的版本。它可以等待一个特定的子进程,也可以设置为非阻塞模式。

  • 函数原型pid_t waitpid(pid_t pid, int *wstatus, int options);
  • 参数
    • pid
      • > 0: 等待进程 ID 为 pid 的子进程。
      • -1: 等待任意子进程(等同于 wait())。
    • wstatus:同 wait(),用于存储退出状态。
    • options
      • 0: 阻塞等待。
      • WNOHANG: 非阻塞,如果子进程还未结束,立即返回 0
      • WUNTRACED: 如果子进程进入暂停状态,也返回。
3. 获取子进程退出状态

wait()waitpid() 返回后,wstatus 中包含了子进程的退出信息,不能直接当作退出码使用。

需要用一组宏来解析:

  • WIFEXITED(wstatus): 判断子进程是否正常退出(通过 return, exit, _exit)。
    • WEXITSTATUS(wstatus): 如果正常退出,用此宏获取退出状态码
  • WIFSIGNALED(wstatus): 判断子进程是否被信号终止
    • WTERMSIG(wstatus): 如果是,用此宏获取终止它的信号编号
  • WIFSTOPPED(wstatus): 判断子进程是否处于暂停状态
    • WSTOPSIG(wstatus): 如果是,用此宏获取使它暂停的信号编号

以下代码演示了如何使用 waitpid() 和状态宏来详细判断子进程的退出原因

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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char const *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork error!\n");
return -1;
} else if (pid == 0) { // 子进程
sleep(3);
printf("子进程结束!\n");
return 66; // 结束子进程,退出码为66
} else { // 父进程
printf("父进程正在等待子进程(pid=%d)结束!\n", pid);

int wait_status = 0;
// 等待指定的子进程pid结束
waitpid(pid, &wait_status, 0);

// 判断子进程是否正常退出
if (WIFEXITED(wait_status)) {
printf("子进程正常退出!\n");
printf("退出状态码:%d\n", WEXITSTATUS(wait_status)); // 获取子进程的退出码
}
// 判断子进程是否被信号杀死
if (WIFSIGNALED(wait_status)) {
printf("子进程被信号杀死!\n");
printf("杀死子进程的信号为:%d\n", WTERMSIG(wait_status));
}
// 判断子进程是否被信号暂停
if (WIFSTOPPED(wait_status)) {
printf("子进程被信号暂停!\n");
printf("暂停子进程的信号为:%d\n",
WSTOPSIG(wait_status)); // 获取暂停子进程的信号
}
// 判断子进程是否被信号继续
if (WIFCONTINUED(wait_status)) {
printf("子进程从暂停态中被恢复了\n");
}
return 0;
}

进程的退出(return, exit, _Exit)

进程的退出分为正常退出和异常退出。这里主要讨论三种正常退出的方式。

  1. return

    • main 函数中使用 return,等同于调用 exit(return_value)。进程会正常终止,并执行清理操作。
    • 普通函数中使用 return仅仅是将控制权返回给调用者,并不会导致进程退出。
  2. exit() 函数

    • 函数原型void exit(int status);
    • 这是标准的退出函数。无论在哪个函数中调用,都会立即终止整个进程
    • 特点:在进程退出前,会执行一系列“清理操作”,包括:
      • 刷新标准 I/O 缓冲区(如 printf 的内容)。
      • 调用 atexit() 注册的函数。
  3. _exit()_Exit() 函数

    • 函数原型void _exit(int status);
    • 这两个函数功能相同,会立即终止进程,但不会执行任何清理操作。缓冲区的数据会丢失,atexit 注册的函数也不会被调用。
    • 推荐场景:在 fork() 后的子进程中,推荐使用 _exit()。因为 exit() 的缓冲区刷新可能会影响父进程的 I/O 流,导致数据重复输出或混乱。使用 _exit() 可以避免这种副作用。

下面的代码演示了在普通函数 PTask1_PlayGame 中,三种退出方式的示例代码

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
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int PTask1_PlayGame(void);

int main(int argc, char const *argv[]) {
pid_t pid = fork();
if (pid < 0) {
perror("fork error!\n");
return -1;
} else if (pid == 0) { // 子进程
PTask1_PlayGame();
// 如果PTask1_PlayGame是return返回的,这里会继续执行
printf("PTask1_PlayGame函数返回了,但子进程还在运行!\n");
sleep(3);
printf("子进程现在才真正退出!\n");
return 0;
} else { // 父进程
int wait_status = 0;
wait(&wait_status);
if (WIFEXITED(wait_status)) {
printf("父进程:子进程已正常退出,退出码:%d\n", WEXITSTATUS(wait_status));
}
}
}

int PTask1_PlayGame(void) {
int i = 0;
for (i = 1; i <= 3; i++) {
printf("PTask1_PlayGame(PID: %d)...玩了%d秒...\n", getpid(), i);
sleep(1);
}
printf("PTask1_PlayGame函数结束了(Game Over)!\n");

// 演示1: return 只是返回main函数,进程继续
// return 11;

// 演示2: exit 会终止整个进程,父进程会收到退出码66
exit(66);

// 演示3: _exit 也会终止整个进程,父进程收到退出码88
// _exit(88);
}

多进程的使用

在实际应用中,可以通过创建多个子进程来并行处理不同的任务,以提高程序的效率和模块化程度。例如,一个车载系统可以为每个功能(如胎压监测、车速显示、油耗计算等)创建一个独立的进程。

  • 实现思路:
    1. 父进程在一个循环中多次调用 fork() 来创建所需数量的子进程。
    2. 在子进程的代码块中,通过循环变量或其他标识来区分不同的子进程,并让它们执行各自的任务函数。
    3. 每个子进程任务完成后,应调用 _exit() 退出,避免不必要的资源清理副作用。
    4. 父进程在创建完所有子进程后,进入另一个循环,调用 wait() 来回收所有子进程的资源,防止它们变成僵尸进程。
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
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

// 模拟各个任务
void tire_pressure_task() { printf("PID %d: 胎压监测任务启动...\n", getpid()); sleep(2); _exit(1); }
void speed_task() { printf("PID %d: 车速监测任务启动...\n", getpid()); sleep(3); _exit(2); }
void fuel_task() { printf("PID %d: 油耗监测任务启动...\n", getpid()); sleep(4); _exit(3); }

int main() {
const int NUM_TASKS = 3;
for (int i = 0; i < NUM_TASKS; i++) {
pid_t pid = fork();
if (pid == 0) { // 子进程
if (i == 0) tire_pressure_task();
else if (i == 1) speed_task();
else if (i == 2) fuel_task();
}
}

// 父进程回收所有子进程
for (int i = 0; i < NUM_TASKS; i++) {
int status;
pid_t child_pid = wait(&status);
if (WIFEXITED(status)) {
printf("父进程:回收了子进程 %d,退出码 %d\n", child_pid, WEXITSTATUS(status));
}
}
printf("所有车载任务已结束。\n");
return 0;
}

程序的加载(system, popen, exec 系列函数)

有时候,一个进程需要执行一个全新的程序。这可以通过加载外部程序来实现

1. system() 函数

system() 函数非常方便,它可以直接执行一个 shell 命令字符串。

  • 函数原型int system(const char *command);
  • 工作原理system() 会创建一个子进程来执行 sh -c "command",并阻塞等待该命令执行完毕。
  • 优点:使用简单,可以执行复杂的 shell 命令,如管道、重定向。
  • 缺点:开销较大(创建了 shell 进程),且存在安全风险(命令注入)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[]) {
printf("--- 开始执行system命令 ---\n");

// 1. 加载并执行外部程序,并传递参数
system("./test_app arg1 arg2");

// 2. 执行系统命令,如ls
system("ls -l /");

// 3. 让程序在后台运行 (&)
system("gedit &"); // gedit会打开,但system不会立即返回,通常等待shell退出

printf("--- system命令执行完毕 ---\n");
return 0;
}
2. popen() 和 pclose() 函数

popen() 函数用于执行一个命令,但与 system() 不同的是,它会创建一个管道(pipe),使得当前进程可以读取该命令的输出,或者写入数据给该命令的输入。

  • 函数原型
    • FILE *popen(const char *command, const char *type);
    • int pclose(FILE *stream);
  • 参数 type:
    • "r": 读取模式。可以像读文件一样,读取 command 的标准输出。
    • "w": 写入模式。可以像写文件一样,将数据写入到 command 的标准输入。
  • pclose():必须调用 pclose() 来关闭 popen 创建的流,它会等待命令执行结束。
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
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[]) {
FILE *fp;
char buffer[1024];

// --- 演示 "r" 模式:读取命令的输出 ---
printf("--- 读取 'ls -l' 的输出 ---\n");
fp = popen("ls -l", "r");
if (fp == NULL) {
perror("popen 'r' error");
return -1;
}
// 逐行读取并打印
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
pclose(fp); // 关闭管道并等待命令结束

// --- 演示 "w" 模式:向命令写入数据 ---
printf("\n--- 向 'grep hello' 写入数据 ---\n");
fp = popen("grep hello", "w");
if (fp == NULL) {
perror("popen 'w' error");
return -1;
}
// 写入数据,grep会筛选包含"hello"的行并输出到终端
fprintf(fp, "hello world\n");
fprintf(fp, "goodbye\n");
fprintf(fp, "another hello line\n");
pclose(fp); // 关闭管道,刷新缓冲区,等待命令结束

return 0;
}
3. exec 系列函数

exec 系列函数是最核心的程序加载机制。它不会创建新进程,而是用一个全新的程序替换当前进程的内存空间(包括代码、数据、堆栈)。

  • 核心特点:一旦 exec 调用成功,原来的程序就不存在了,PID 保持不变,但执行的已经是新程序了。exec 成功后不会返回
  • 常见模式fork() + exec()。父进程 fork 一个子进程,然后子进程调用 exec 去执行新程序,父进程可以继续做自己的事或 wait 子进程。systempopen 内部都封装了这个模式。
  • 函数家族
    • execl, execlp, execle: 参数以列表形式给出 (l)。(list)
    • execv, execvp, execve: 参数以字符串数组形式给出 (v)。(vector)
    • p 的 (execlp, execvp) 会在 PATH 环境变量中搜索可执行文件。(path)
    • e 的 (execle, execve) 允许你自定义新程序的环境变量。(environment)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 简单的 fork + exec 示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
pid_t pid = fork();
if (pid == 0) { // 子进程
printf("子进程(PID:%d)开始执行 'ls -l'\n", getpid());
// 使用新程序 "ls" 替换当前子进程
execlp("ls", "ls", "-l", NULL);
// 如果execlp成功,下面这行代码永远不会被执行
perror("execlp failed");
return 1;
} else if (pid > 0) { // 父进程
printf("父进程(PID:%d)等待子进程结束...\n", getpid());
wait(NULL);
printf("父进程:子进程执行完毕。\n");
}
return 0;
}

进程的关系(重点)

(1) 父子进程

通过 fork() 创建的进程具有父子关系。父进程是创建者,子进程是被创建者。父进程有责任管理其子进程的生命周期,最重要的是在子进程结束后,通过调用 wait()waitpid() 来回收其资源,防止子进程变成僵尸进程。

可以使用 pstree 命令查看系统中所有进程形成的“家族树”,直观地看到父子关系。topps 命令也可以查看每个进程的 PID (进程 ID) 和 PPID (父进程 ID)。

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

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

if (pid < 0) {
perror("fork error");
return -1;
} else if (pid == 0) { // 子进程
printf("我是子进程(PID:%d),我的父进程是(PPID:%d)\n", getpid(), getppid());
sleep(5);
printf("子进程结束。\n");
return 0;
} else { // 父进程
printf("我是父进程(PID:%d),我创建了子进程(PID:%d)\n", getpid(), pid);
wait(NULL); // 阻塞等待,直到子进程结束并回收其资源
printf("父进程:已回收子进程资源,现在退出。\n");
}
return 0;
}

(2) 孤儿进程

  • 说明:当一个父进程比它的子进程先结束时,这个子进程就成为了“孤儿进程”。
  • 系统处理:孤儿进程不会无人管理,它会被系统的 1 号进程(在现代 Linux 系统中通常是 systemd)所“收养”,systemd 会成为它的新父进程,并负责在其结束后回收资源。
  • 注意:虽然系统会自动处理孤儿进程,但这通常意味着程序设计上存在逻辑问题。正常情况下,父进程应该等待所有子进程结束后再退出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

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

if (pid < 0) {
perror("fork error");
return -1;
} else if (pid == 0) { // 子进程
// 循环打印自己的PID和父进程的PPID
while (1) {
printf("我是子进程(PID:%d),我的父进程是(PPID:%d)\n", getpid(), getppid());
sleep(1);
}
} else { // 父进程
printf("我是父进程(PID:%d),我将在5秒后退出。\n", getpid());
sleep(5);
printf("父进程结束!\n");
}
return 0;
}

运行现象:程序运行后,子进程会持续打印其父进程的 ID。5 秒后,父进程退出,你会观察到子进程打印的父进程 ID(PPID)从原来的父进程 PID 变成了 1

(3) 僵尸进程

  • 说明:当一个子进程比父进程先结束,但父进程没有调用 wait()waitpid() 来获取子进程的退出状态时,子进程就会变成“僵尸进程”。
  • 状态:此时,子进程的绝大部分资源(如内存)已被释放,但在内核的进程表中仍然保留着它的条目(包含 PID、退出状态、资源使用信息等),等待父进程来读取。在 ps 命令中,僵尸进程的状态显示为 Z (Zombie) 或 <defunct>
  • 危害:僵尸进程本身不占用太多资源,但它会一直占用一个 PID。如果一个程序持续产生僵尸进程而不进行回收,最终会耗尽系统可用的 PID,导致无法创建任何新进程,从而使系统瘫痪。必须避免僵尸进程的产生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main(void) {
// 1. 创建10个子进程
for (int i = 0; i < 10; i++) {
pid_t pid = fork();
if (pid == 0) { // 子进程
printf("子进程(PID:%d)创建并立即退出。\n", getpid());
_exit(0); // 子进程立刻退出
}
}

// 2. 父进程不回收子进程,而是长时间运行
printf("父进程(PID:%d)正在运行,但未回收子进程。请在另一个终端使用 'ps aux | grep a.out' 查看。\n", getpid());
getchar(); // 阻塞在这里,等待用户输入
printf("父进程结束。\n");
return 0;
}

运行现象:运行该程序后,立即在另一个终端执行 ps aux | grep a.out,你会看到多个状态为 Z+ 的僵尸进程

(4) 守护进程 (精灵进程)

  • 说明:守护进程是一种特殊的后台进程,它完全脱离于控制终端,独立于任何登录会话,通常用于执行周期性任务或提供系统服务(如 sshd, httpd 等)。
  • 特点
    1. 长期运行:通常在系统启动时开始运行,直到系统关闭。
    2. 无控制终端:不与任何终端关联,不会接收终端输入或向终端输出。
    3. 会话独立:自己是一个独立的会话首进程。
    4. 后台运行:在系统后台默默执行任务。
    5. 特定工作目录:通常将工作目录切换到根目录 (/),以防其所在的文件系统被卸载。
    6. 特定文件权限掩码:通常将 umask 设置为 0,以便对创建的文件拥有完全控制权。
  • 创建步骤
    1. fork() 创建子进程,父进程退出。
    2. 子进程调用 setsid() 创建新会话,使自己成为会话首进程、进程组组长,并脱离控制终端。
    3. 调用 chdir("/") 改变工作目录到根目录。
    4. 调用 umask(0) 重设文件权限掩码。
    5. 关闭不再需要的文件描述符(特别是标准输入、输出、错误)。
  • 简化函数<unistd.h> 提供了 daemon() 函数来简化上述步骤。
    • int daemon(int nochdir, int noclose);
    • nochdir: 若为 0,则改变目录到 /
    • noclose: 若为 0,则将标准输入、输出、错误重定向到 /dev/null
    • 成功返回 0,失败返回-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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
// 1. 调用daemon函数,成为守护进程
// chdir到根目录,关闭标准IO
if (daemon(0, 0) == -1) {
perror("daemon create failed");
exit(1);
}

// 2. 守护进程的核心逻辑
FILE *fp;
time_t t;

while(1) {
fp = fopen("/tmp/daemon_time.log", "a"); // 追加模式打开日志文件
if (fp) {
t = time(NULL);
fprintf(fp, "Current Time: %s", asctime(localtime(&t)));
fclose(fp);
}
sleep(1);
}

return 0;
}

运行现象:编译运行后,程序会立即返回,你看不到任何输出。但你可以通过 ps aux | grep a.out 找到这个进程在后台运行,并且 /tmp/daemon_time.log 文件会每秒被写入一次当前时间

其他的进程相关函数说明

进程创建(vfork 函数)

vfork() 是一个历史遗留的函数,其设计初衷是在 fork() 之后立即调用 exec() 的场景下提供更高的效率。由于现代 Linux 内核对 fork() 实现了“写时复制(Copy-On-Write)”优化,vfork() 的性能优势已不再明显,且其独特的行为容易导致错误,因此不推荐使用

vfork()fork() 的核心区别:

  1. 内存复制方式

    • fork():子进程会获得父进程数据段、堆、栈的独立副本(通过写时复制技术实现)。
    • vfork():子进程与父进程共享数据段。子进程对共享内存(如全局变量)的修改会直接影响到父进程。
  2. 执行顺序

    • fork():父子进程的执行顺序由操作系统调度决定,是不确定的。
    • vfork()保证子进程先运行,父进程会被阻塞,直到子进程调用了 exec() 系列函数或 _exit() 退出。

使用注意

  • 因为共享地址空间,在 vfork() 创建的子进程中,不能从函数 return 或调用 exit(),这会破坏父进程的栈帧。必须使用 _exit()exec() 族函数来结束子进程
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 <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int g_num = 100; // 全局变量

int main(void) {
pid_t pid = vfork();

if (pid < 0) {
perror("vfork error");
return -1;
} else if (pid == 0) { // 子进程
printf("--- 子进程开始执行 ---\n");
printf("子进程中:修改前 g_num = %d\n", g_num);
g_num = 200; // 修改全局变量
printf("子进程中:修改后 g_num = %d\n", g_num);
printf("--- 子进程退出 ---\n");
_exit(0); // 必须用 _exit()
} else { // 父进程
// 父进程会等待子进程结束后才执行
printf("--- 父进程开始执行 ---\n");
printf("父进程中:g_num = %d\n", g_num);
}
return 0;
}

运行结果

1
2
3
4
5
6
--- 子进程开始执行 ---
子进程中:修改前 g_num = 100
子进程中:修改后 g_num = 200
--- 子进程退出 ---
--- 父进程开始执行 ---
父进程中:g_num = 200

结果清晰地表明:子进程先执行,并且它对 g_num 的修改反映到了父进程中

进程间的通信

在现代操作系统中,为了安全和稳定,每个进程都拥有自己独立的虚拟内存空间。这意味着一个进程无法直接访问另一个进程的内存,这种机制被称为进程隔离

它保证了一个进程的崩溃不会影响到其他进程,但同时也带来了挑战:进程之间如何协作和交换数据?image

很多复杂应用,如 Web 服务器处理用户请求、数据库系统管理并发访问,都需要多个进程协同工作。为了解决进程隔离下的协作问题,操作系统内核提供了一系列官方的、安全可控的“沟通渠道”,即进程间通信(Inter-Process Communication, IPC) 机制。

这些机制本质上都是在内核空间开辟一块共享区域,允许不同进程通过这块区域进行数据交换或同步,如下图所示:image

有哪些通信方式(重点):

Linux 系统提供了多种 IPC 方式,各有其特点和适用场景:

  • (1) 文件和文件锁 (File Locking): 通过读写共享文件进行通信,使用文件锁同步,速度慢。
  • (2) 管道 (Pipe): 包括只能用于亲缘进程的匿名管道和可用于任意进程的命名管道 (FIFO)
  • (3) 信号 (Signal): 用于异步通知,不传输数据,类似于系统中断。
  • (4) 消息队列 (Message Queue): 内核维护的消息链表,适用于结构化、小规模数据通信。 (System V IPC)
  • (5) 共享内存 (Shared Memory): 最快的 IPC 方式,将同一块物理内存映射到多个进程,适用于大规模数据交换。 (System V IPC)
  • (6) 信号量 (Semaphore): 主要用于进程/线程间的同步与互斥,保护共享资源(如共享内存)。 (System V IPC)
  • (7) 套接字 (Socket): 最通用的 IPC 机制,既可用于本机进程间通信,也可用于网络间通信

(1) 文件与文件锁

这是最直观的 IPC 方式。一个进程将数据写入文件,另一个进程从文件中读取。为防止并发读写造成数据损坏,必须使用文件锁。

  • 原理:利用文件系统作为公共存储区。
  • 同步flock()函数提供建议性锁。LOCK_EX(独占锁/写锁)保证只有一个进程可以写入,LOCK_SH(共享锁/读锁)允许多个进程同时读取。
1. flock 函数
  • 头文件: #include <sys/file.h>
  • 原型: int flock(int fd, int operation);
  • operation:
    • LOCK_SH: 施加共享锁
    • LOCK_EX: 施加独占锁
    • LOCK_UN: 释放锁
    • LOCK_NB: 与以上选项组合,使调用非阻塞
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
// writer.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>
#include <time.h>
#include <string.h>

#define FILE_PATH "/tmp/msg_file.txt"

int main(void) {
int fd = open(FILE_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) { perror("open"); return -1; }

char buf[512];
printf("--- 写进程 (PID: %d) ---\n", getpid());
while (1) {
printf("输入消息 (输入 'quit' 退出): ");
fgets(buf, sizeof(buf), stdin);

flock(fd, LOCK_EX); // 获取写锁

lseek(fd, 0, SEEK_SET);
ftruncate(fd, 0); // 清空文件
write(fd, buf, strlen(buf));

flock(fd, LOCK_UN); // 释放锁

if (strncmp(buf, "quit", 4) == 0) break;
}
close(fd);
return 0;
}
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
// reader.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>

#define FILE_PATH "/tmp/msg_file.txt"

int main(void) {
int fd = open(FILE_PATH, O_RDONLY | O_CREAT, 0666);
if (fd == -1) { perror("open"); return -1; }

char buf[512] = {0};
printf("--- 读进程 (PID: %d) ---\n", getpid());
while (1) {
flock(fd, LOCK_SH); // 获取读锁

lseek(fd, 0, SEEK_SET);
int bytes_read = read(fd, buf, sizeof(buf) - 1);
if (bytes_read > 0) {
buf[bytes_read] = '\0';
printf("读取到: %s", buf);
}

flock(fd, LOCK_UN); // 释放锁

if (strncmp(buf, "quit", 4) == 0) break;
sleep(1);
}
close(fd);
return 0;
}

(2)、通过管道 (Pipe) 来通信

管道是 Linux 中一种基于文件的 IPC 机制,它在内核中创建一块缓冲区,用于进程间的单向数据传输,遵循“先进先出”(FIFO)原则。

A、匿名管道 (Anonymous Pipe)
1、说明
  • 用途:只能用于具有亲缘关系的进程之间(通常是父子进程)。
  • 特点:它没有文件系统中的路径名,存在于内存中,随进程的结束而消失。
  • 核心:必须在 fork() 之前调用 pipe() 创建管道,子进程会继承父进程的文件描述符,从而实现共享。通信时,父子进程通常会各自关闭管道的一个端口(一个关读,一个关写)。
2、图解

image

3、pipe() 函数的使用

使用命令:man 2 pipe 查看接口函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数头文件:
#include <unistd.h>

// 函数原型:
int pipe(int pipefd[2]);
/*
参数一:一个含两个整数的数组。
pipefd[0]:管道的读取端文件描述符。
pipefd[1]:管道的写入端文件描述符。
*/

// 返回值:
// 成功返回 0,失败返回 -1。
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
/**
* @brief 使用匿名管道实现父进程向子进程发送消息。
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(void) {
int pipe_fd[2];
if (pipe(pipe_fd) == -1) {
perror("pipe create failed");
return -1;
}

pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
return -1;
}

if (pid == 0) { // 子进程:读取数据
close(pipe_fd[1]); // 关闭写端
char buf[64];
int len = read(pipe_fd[0], buf, sizeof(buf) - 1);
buf[len] = '\0';
printf("子进程收到消息: '%s'\n", buf);
close(pipe_fd[0]);
} else { // 父进程:写入数据
close(pipe_fd[0]); // 关闭读端
char *msg = "Message from parent!";
write(pipe_fd[1], msg, strlen(msg));
printf("父进程已发送消息。\n");
close(pipe_fd[1]);
wait(NULL); // 等待子进程结束
}
return 0;
}
B、命名管道 (Named Pipe / FIFO)
1、说明
  • 用途:可以在任意两个不相关的进程之间进行通信。
  • 特点:它在文件系统中以一个特殊文件的形式存在(文件类型为p),有具体的路径名。只要进程有权限访问这个文件,就可以进行通信。
  • 阻塞特性:打开命名管道时,读端会阻塞直到写端被打开,反之亦然。
2、创建命令与函数
  • 终端命令mkfifo <管道文件名>,例如 mkfifo /tmp/myfifo
  • mkfifo() 函数man 3 mkfifo 查看接口函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数头文件:
#include <sys/types.h>
#include <sys/stat.h>

// 函数原型:
int mkfifo(const char *pathname, mode_t mode);
/*
参数一:pathname, 管道文件的路径名。
参数二:mode, 文件权限,如 0666。
*/

// 返回值:
// 成功返回 0,失败返回 -1。
3、示例代码:不相关进程通信

a、写入命名管道 (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
/**
* @brief 向命名管道写入数据。
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FIFO_PATH "/tmp/myfifo"

int main(void) {
// 如果管道不存在则创建
if (access(FIFO_PATH, F_OK) == -1) {
mkfifo(FIFO_PATH, 0666);
}

printf("写进程:等待读进程连接...\n");
int fd = open(FIFO_PATH, O_WRONLY);
printf("写进程:已连接,发送消息。\n");

char *msg = "Hello from FIFO!";
write(fd, msg, strlen(msg));

close(fd);
return 0;
}

b、读取命名管道 (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
/**
* @brief 从命名管道读取数据。
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

#define FIFO_PATH "/tmp/myfifo"

int main(void) {
printf("读进程:等待写进程连接...\n");
int fd = open(FIFO_PATH, O_RDONLY);
printf("读进程:已连接,等待消息。\n");

char buf[64];
int len = read(fd, buf, sizeof(buf) - 1);
buf[len] = '\0';
printf("读进程收到消息: '%s'\n", buf);

close(fd);
// 可选:删除管道文件
// unlink(FIFO_PATH);
return 0;
}

如何运行

  1. 编译两个程序:gcc writer.c -o writergcc reader.c -o reader
  2. 在一个终端先运行 ./reader,它会阻塞。
  3. 在另一个终端再运行 ./writer,它会写入数据并退出
  4. 此时,reader 终端会打印出收到的消息并退出

(3)、通过信号 (Signal) 来通信

信号是一种异步通信机制,用于通知进程某个事件已经发生。它不传输数据,功能类似于一个软件中断,用于进程控制和状态通知。

1、信号命令与种类
A、信号的种类
  • 查看命令: man 7 signalkill -l
  • 常用信号:
    • SIGINT (2): 中断信号 (Ctrl+C)。
    • SIGKILL (9): 强制杀死信号,不可被捕捉或忽略。
    • SIGTERM (15): 终止信号 (kill 默认发送)。
    • SIGCHLD (17): 子进程状态改变。
    • SIGSTOP (19): 暂停信号,不可被捕捉或忽略。
    • SIGALRM (14): 定时器信号。
    • SIGUSR1, SIGUSR2: 用户自定义信号。
B、终端发送信号
  • kill -<信号值> <PID>: 向指定进程发送信号。
  • killall -<信号值> <进程名>: 向所有同名进程发送信号。
2、信号编程核心函数
kill(): 发送信号 (man 2 kill)
1
2
#include <signal.h>
int kill(pid_t pid, int sig);
alarm(): 设置定时器信号 (man 2 alarm)
1
2
#include <unistd.h>
unsigned int alarm(unsigned int seconds); // seconds秒后发送 SIGALRM
signal(): 设置信号处理方式 (man 2 signal)
1
2
3
4
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// handler 可以是 SIG_DFL (默认), SIG_IGN (忽略), 或自定义函数地址。
3、示例代码
a、处理终止信号 (捕捉信号)
  • 用途:允许程序在收到 Ctrl+C 等终止信号时,执行清理工作(如保存数据)后再优雅地退出。
  • 说明:捕捉 SIGINTSIGQUIT 信号。
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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t running = 1;

void term_handler(int sig) {
printf("\n收到信号 %d,准备退出...\n", sig);
running = 0;
}

int main(void) {
signal(SIGINT, term_handler); // 捕捉 Ctrl+C
signal(SIGQUIT, term_handler); // 捕捉 Ctrl+\

printf("程序运行中,按 Ctrl+C 或 Ctrl+\\ 退出。\n");
while(running) {
printf("工作中...\n");
sleep(1);
}

printf("执行清理任务...\n");
// ... 清理代码 ...
printf("程序已退出。\n");
return 0;
}
b、处理闹钟信号 (捕捉信号)
  • 用途:实现超时机制或周期性任务。
  • 说明:使用 alarm() 配合 SIGALRM 信号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler(int sig) {
printf("闹钟触发!执行定时任务。\n");
alarm(3); // 重新设置下一次闹钟,实现周期性
}

int main(void) {
signal(SIGALRM, alarm_handler);
alarm(3); // 3秒后第一次触发

printf("已设置3秒周期闹钟,等待触发...\n");
while(1) {
pause(); // 等待信号,降低CPU消耗
}
return 0;
}
c、忽略信号
  • 用途:保护关键进程不被某些信号(如 Ctrl+C)意外中断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main(void) {
// 忽略 SIGINT 信号(ctrl+c、ctrl+\)
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
printf("已忽略 Ctrl+C 信号,请尝试按 Ctrl+C。\n");
printf("请使用 kill -9 %d 强制终止。\n", getpid());
while(1) {
sleep(1);
}
return 0;
}
d、处理通信信号
  • 用途:实现父子进程或任意两个进程间的简单通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void child_sig_handler(int sig) {
printf("子进程:收到父进程发来的 SIGUSR1 信号!\n");
}

int main(void) {
pid_t pid = fork();
if (pid == 0) { // 子进程
signal(SIGUSR1, child_sig_handler);
printf("子进程 (PID: %d) 等待信号...\n", getpid());
while(1) pause();
} else { // 父进程
sleep(1); // 确保子进程已设置好信号处理
printf("父进程:向子进程 %d 发送 SIGUSR1 信号。\n", pid);
kill(pid, SIGUSR1);
wait(NULL);
}
return 0;
}
e、处理子进程的退出
  • 用途:父进程通过 SIGCHLD 信号异步回收子进程资源,避免产生僵尸进程,且父进程无需阻塞。
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
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void child_handler(int sig) { // 信号处理不要太复杂(运算、时间等)
int wait_status = 0; // 返回的状态
pid_t child_pid;

while ((child_pid = waitpid(-1, &wait_status, WNOHANG | WUNTRACED)) >
0) { // 等待任意任意子进程,如果还有子进程需要回收,就一直循环
if (WIFEXITED(wait_status)) {
printf("子进程(PID:%d)正常退出,状态:%d\n", child_pid,
WEXITSTATUS(wait_status));
} else if (WIFSIGNALED(wait_status)) {
printf("子进程(PID:%d)被信号%d杀死\n", child_pid, WTERMSIG(wait_status));
} else if (WIFSTOPPED(wait_status)) {
printf("子进程(PID:%d)被信号%d暂停\n", child_pid, WSTOPSIG(wait_status));
}
}
}

// 主函数
int main(int argc, char const* argv[]) {
pid_t pid = fork();
if (pid < 0) { // 创建进程失败
perror("fork error!\n");
return -1;
} else if (pid == 0) { // 子进程
int i = 0;
while (1) {
printf("子进程(PID: %d)正在运行中.......\n", getpid());
sleep(1);

i++;
if (i == 100) {
printf("子进程结束!\n");
_exit(0);
}
}
} else { // 父进程
// 使用signal,设置信号处理函数
signal(SIGCHLD, child_handler);
while (1) {
printf("父进程(PID: %d)正在运行中.......\n", getpid());
sleep(1);
}
return 0;
}

return 0;
}

f、处理自定义工作进程控制
  • 用途:使用用户自定义信号 SIGUSR1SIGUSR2 来灵活控制一个工作进程的状态(如暂停/继续/终止)。
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 <signal.h>
#include <unistd.h>
#include <stdlib.h>

volatile sig_atomic_t is_paused = 0;

void control_handler(int sig) {
if (sig == SIGUSR1) {
is_paused = !is_paused; // 切换暂停/继续状态
printf("收到 SIGUSR1, 状态切换为: %s\n", is_paused ? "暂停" : "继续");
} else if (sig == SIGUSR2) {
printf("收到 SIGUSR2, 进程终止。\n");
_exit(0);
}
}

int main(void) {
signal(SIGUSR1, control_handler);
signal(SIGUSR2, control_handler);

printf("工作进程 (PID: %d) 已启动。\n", getpid());
printf("使用 kill -SIGUSR1 %d 切换暂停/继续。\n", getpid());
printf("使用 kill -SIGUSR2 %d 终止进程。\n", getpid());

while(1) {
if (!is_paused) {
printf("工作中...\n");
}
sleep(1);
}
return 0;
}
g、信号控制多线程 (暂无)

(此部分内容待后续线程章节学习后补充)

(4)、System V IPC(重点)

System V IPC 是一组经典且功能强大的进程间通信机制,包括消息队列共享内存信号量。它们共享一套相似的设计哲学和管理方式。

核心概念与管理
1. 工作流程

所有 System V IPC 机制都遵循相似的步骤:

  1. 创建密钥 (Key):使用 ftok() 函数,通过一个已存在的文件路径和项目 ID,生成一个唯一的 key_t 类型的键值。这个键值是不同进程用来访问同一个 IPC 对象的“门牌号”。
  2. 创建/获取 IPC 对象:使用 msgget()shmget()semget() 函数,传入密钥来创建新的 IPC 对象,或获取一个已存在的对象的 ID。
  3. 操作 IPC 对象:使用各自的 API(如 msgsnd/msgrcv, shmat, semop)进行数据交换或同步。
  4. 销毁 IPC 对象:通信结束后,使用 msgctl()shmctl()semctl() 来删除 IPC 对象,释放内核资源。
2. IPC 管理命令
  • ipcs -a: 查看系统中所有的 IPC 对象(消息队列、共享内存、信号量)。
  • ipcs -q: 单独查看消息队列。
  • ipcs -m: 单独查看共享内存。
  • ipcs -s: 单独查看信号量。
  • ipcrm -q <id>: 删除指定 ID 的消息队列。
  • ipcrm -m <id>: 删除指定 ID 的共享内存。
  • ipcrm -s <id>: 删除指定 ID 的信号量。
3. ftok() 函数:创建密钥

man 3 ftok 查看接口函数。

1
2
3
4
5
6
7
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
/*
参数一:一个已存在的文件路径。
参数二:项目ID,一个非零整数。
返回值:成功返回一个 key_t 键值,失败返回 -1。
*/
A、消息队列 (Message Queue)
1. 说明与图解

消息队列是存放在内核中的一个消息链表,允许一个或多个进程向其发送或接收结构化的消息。它克服了管道只能传输无格式字节流的限制。消息队列图解

2. 核心函数
  • msgget(): 创建或获取消息队列 ID (man 2 msgget)

    1
    2
    3
    #include <sys/msg.h>
    int msgget(key_t key, int msgflg);
    // msgflg: 表示创建或打开队列的权限和方式,通常是 IPC_CREAT | 0666
  • msgsnd(): 发送消息 (man 2 msgsnd)

    1
    2
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    // 参数分别为: 消息id 消息本体 消息大小 发送方式,一般为0
  • msgrcv(): 接收消息 (man 2 msgrcv)

    1
    2
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    // 参数分别为: 消息id 消息本体 消息大小 消息类型 发送方式,一般为0
  • msgctl(): 控制消息队列 (man 2 msgctl)

    1
    2
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    // cmd: IPC_RMID 用于删除消息队列
3. 示例代码
  • 公共结构体定义 (common.h)

    1
    2
    3
    4
    5
    6
    7
    8
    // common.h
    #define MSG_KEY_PATH "."
    #define MSG_KEY_PROJ_ID 'a'

    struct msg_buf {
    long mtype; // 消息类型,必须 > 0
    char mtext[128]; // 消息内容
    };
  • 发送端 (sender.c)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // sender.c
    #include <stdio.h>
    #include <sys/msg.h>
    #include "common.h"

    int main() {
    key_t key = ftok(MSG_KEY_PATH, MSG_KEY_PROJ_ID);
    int msqid = msgget(key, IPC_CREAT | 0666);

    struct msg_buf msg = {1, "Hello from sender!"}; // 消息类型为 1
    msgsnd(msqid, &msg, sizeof(msg.mtext), 0);

    printf("消息已发送。\n");
    return 0;
    }
  • 接收端 (receiver.c)

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

    int main() {
    key_t key = ftok(MSG_KEY_PATH, MSG_KEY_PROJ_ID);
    int msqid = msgget(key, IPC_CREAT | 0666);

    struct msg_buf msg;
    msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0); // 接收类型为 1 的消息

    printf("收到消息: %s\n", msg.mtext);

    // 删除消息队列
    msgctl(msqid, IPC_RMID, NULL);
    return 0;
    }
B、共享内存 (Shared Memory)
1. 说明与图解

共享内存是最快的 IPC 方式。它将一块物理内存映射到多个进程的虚拟地址空间中,进程可以直接像访问普通变量一样读写这块内存,无需内核介入数据拷贝。共享内存图解

2. 核心函数
  • shmget(): 创建或获取共享内存 ID (man 2 shmget)

    1
    2
    3
    #include <sys/shm.h>
    int shmget(key_t key, size_t size, int shmflg);
    // size: 共享内存大小,通常是页大小的整数倍。
  • shmat(): 映射共享内存到进程地址空间 (man 2 shmat)

    1
    2
    void *shmat(int shmid, const void *shmaddr, int shmflg);
    // 返回值是映射到本进程的内存地址指针。
  • shmdt(): 解除映射 (man 2 shmdt)

    1
    int shmdt(const void *shmaddr);
  • shmctl(): 控制共享内存 (man 2 shmctl)

    1
    2
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    // cmd: IPC_RMID 用于删除共享内存段。
3. 示例代码
  • 写进程 (writer.c)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // writer.c
    #include <stdio.h>
    #include <string.h>
    #include <sys/shm.h>

    int main() {
    key_t key = ftok(".", 'b');
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);

    char *shm_ptr = (char *)shmat(shmid, NULL, 0);

    strcpy(shm_ptr, "Hello from shared memory!");
    printf("数据已写入共享内存。\n");

    shmdt(shm_ptr); // 解除映射
    return 0;
    }
  • 读进程 (reader.c)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // reader.c
    #include <stdio.h>
    #include <sys/shm.h>

    int main() {
    key_t key = ftok(".", 'b');
    int shmid = shmget(key, 1024, IPC_CREAT | 0666);

    char *shm_ptr = (char *)shmat(shmid, NULL, 0);

    printf("从共享内存读取数据: %s\n", shm_ptr);

    shmdt(shm_ptr); // 解除映射
    shmctl(shmid, IPC_RMID, NULL); // 删除共享内存
    return 0;
    }
C、信号量 (Semaphore)
1. 说明

信号量本质上是一个计数器,主要用于进程间的同步与互斥,以保护共享资源(如共享内存)不被并发访问破坏。它本身不传输数据。

  • 互斥:确保同一时间只有一个进程能访问临界资源(如厕所只有一把钥匙)。
  • 同步:协调多个进程的执行顺序(如生产者生产完,才能通知消费者消费)。
  • PV 操作
    • P 操作 (sem_wait): 申请资源,信号量计数值减 1。如果计数值为 0,则进程阻塞。
    • V 操作 (sem_post): 释放资源,信号量计数值加 1。
2. POSIX 有名信号量

这是比 System V 信号量更现代、更易用的接口,常用于进程间同步。它在 /dev/shm/ 目录下创建一个文件来表示信号量。

3. 核心函数 (编译时需链接 -pthread)
  • sem_open(): 创建或打开有名信号量 (man 3 sem_open)

    1
    2
    3
    4
    5
    #include <semaphore.h>
    sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
    // name: 信号量名称,格式为 "/somename"。
    // oflag: O_CREAT 创建, O_EXCL 确保是新创建。
    // value: 信号量初始值。
  • sem_wait(): P 操作 (加锁) (man 3 sem_wait)

    1
    int sem_wait(sem_t *sem);
  • sem_post(): V 操作 (解锁) (man 3 sem_post)

    1
    int sem_post(sem_t *sem);
  • sem_close(): 关闭信号量 (man 3 sem_close)

    1
    int sem_close(sem_t *sem);
  • sem_unlink(): 删除有名信号量 (man 3 sem_unlink)

    1
    int sem_unlink(const char *name);
4. 示例代码:使用信号量保护共享内存
  • 公共头文件 (common.h)

    1
    2
    3
    4
    5
    // common.h
    #include <semaphore.h>
    #define SHM_KEY_PATH "."
    #define SHM_KEY_PROJ_ID 'c'
    #define SEM_NAME "/my_semaphore"
  • 初始化进程 (init.c)

    1
    2
    3
    4
    5
    // init.c
    #include "common.h"
    // (包含 shmget/shmat/sem_open 等创建和初始化代码)
    // sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 1); // 初始值为1,用于互斥
    // ...
  • 使用共享资源的进程 (proc.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
    // proc.c
    #include "common.h"
    #include <stdio.h>
    #include <sys/shm.h>
    #include <unistd.h>

    int main() {
    key_t key = ftok(SHM_KEY_PATH, SHM_KEY_PROJ_ID);
    int shmid = shmget(key, sizeof(int), 0);
    int *shared_counter = (int *)shmat(shmid, NULL, 0);

    sem_t *sem = sem_open(SEM_NAME, 0);

    for (int i = 0; i < 5; i++) {
    sem_wait(sem); // P操作:加锁

    // --- 临界区开始 ---
    int temp = *shared_counter;
    printf("PID %d: read %d, ", getpid(), temp);
    temp++;
    sleep(1); // 模拟耗时操作
    *shared_counter = temp;
    printf("write %d\n", *shared_counter);
    // --- 临界区结束 ---

    sem_post(sem); // V操作:解锁
    }

    shmdt(shared_counter);
    sem_close(sem);
    return 0;
    }

    如何运行:先运行一个程序初始化共享内存(设为 0)和信号量(设为 1)。然后同时运行多个 proc 实例。你会看到,由于信号量的保护,共享计数器会从 0、1、2…有序地增加,而不会出现多个进程同时读到相同值导致的“竞争条件”。