
程序 、进程 与虚拟内存之间的核心关系
这张图清晰地展示了 Linux 系统中程序 、进程 与虚拟内存之间的核心关系
1. 左侧:程序(可执行文件)
这是磁盘上的静态文件,包含了代码和数据,主要分为几个段:
.text:存放可执行的机器指令(代码段),是只读的。.data:存放已初始化的全局变量和静态变量。- .rodata:存放只读数据,如字符串常量。
.bss:存放未初始化的全局变量和静态变量,在程序加载时会被自动清零。
这些段只是文件中的数据,本身不代表一个正在运行的实体。
2. 中间:task_struct(进程描述符)
当程序被加载执行时,内核会为它创建一个进程 ,并用 task_struct 结构体来描述这个进程的所有信息,这就是进程的 "身份证"。
- 图中
mm*是一个指向mm_struct的指针,mm_struct是内存描述符,它管理着进程的整个虚拟地址空间。 - 每个进程都有自己独立的
task_struct和mm_struct,这是进程间隔离的基础。
3. 右侧:虚拟内存(进程地址空间)
这是每个进程独立拥有的虚拟地址空间,它是一个逻辑上的连续地址范围,由内核的内存管理单元(MMU)映射到物理内存。
- 程序被 "加载" 到这里:
.text、.data等段会被映射到虚拟地址空间的对应区域。 - 进程的代码执行、数据读写都发生在这个虚拟地址空间中,而不是直接操作物理内存。
task_struct通过mm指针,将进程的执行上下文和它的虚拟内存空间关联起来。
4. 整体流程
- 加载 :内核将磁盘上的程序文件(
.text,.data等段)加载到进程的虚拟地址空间中。 - 关联 :内核创建
task_struct,并通过其中的mm指针,将进程描述符与新创建的虚拟内存空间关联。 - 执行:CPU 执行进程的代码时,通过虚拟地址访问内存,内核负责将虚拟地址透明地转换为物理地址。
简单来说,程序是静态的代码和数据文件,进程是程序的一次执行实例,而虚拟内存则是这个实例运行时的 "舞台",task_struct 则是管理这个舞台的 "管理员"。
查看进程之间的关系(ubuntu中)
pstree
fork函数
Fork函数
https://blog.csdn.net/weixin_73503631/article/details/154989083?spm=1011.2415.3001.5331
⭐⭐⭐⭐⭐
执行fork函数之前,操作系统只有一个进程,fork函数之前的代码只会被执行一次
在执行fork函数之后,操作系统有两个几乎一样的进程,fork函数之后的代码会被执行两次。
子进程的无缝替换
exechan'shu'zu
常用后缀:
l:代表以列表形式传参(list)
v:代表以矢量数组形式传参(vector)
p:代表使用环境变量Path来寻找指定执行文件
e:代表用户提供自定义的环境变量
#include <unistd.h>
函数原型:
int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
int execv(const char *path, char *const argv[])
int execve(const char *path, char *const argv[],char *const envp[])
返回值:
成功:不返回
失败:-1
要点总结
-
l后缀和v后缀必须两者选其一来使用
-
p后缀和e后缀是可选的,可用可不用
-
组合后缀的相关函数还有很多,可自己进一步了解
exce函数有可能执行失败,需要预防
-
新程序的文件路径出错
-
传参或者是自定义环境变量时,没有加NULL
-
新程序没有执行权限
execlp示例:



进程的退出
正常退出:
-
从main函数return
-
调用exit()函数终止(退出时考虑IO缓冲区),标准库函数 (属于
stdlib.h),终止进程前会做善后清理工作; -
调用_exit()函数终止(直接退出,不考虑IO缓冲区),系统调用 (属于
unistd.h),直接终止进程,不做任何清理,是更底层的操作
exit和_exit退出函数
头文件:
#include <unistd.h>
#include <stdlib.h>
原型:
void _exit(int status);
void exit(int status);
返回值:
不返回
#include <stdio.h>
#include <stdlib.h> // exit() 头文件
#include <unistd.h> // _exit() 头文件
int main() {
printf("测试exit和_exit的区别"); // 注意:没有换行符\n,printf会缓存内容
// 测试1:调用exit()
// exit(0); // 执行后,缓冲区会被刷新,能看到打印内容
// 测试2:调用_exit()
_exit(0); // 执行后,缓冲区未刷新,看不到打印内容
}
等待子进程的终结

只有当子进程exit成功退出后,父进程wait ( ) 后面的内容才会执行,否则父进程会一直阻塞在wait这里
- 父进程调用
wait(&status)后,会阻塞在这个位置,暂停执行后续代码。 - 只有当子进程通过
exit(0)正常退出(或被信号终止)后,内核才会唤醒父进程,让它继续执行wait()之后的代码。 - 如果子进程没有退出,父进程会一直阻塞在
wait()这里,直到子进程结束。
wait函数
头文件
#include <sys/wait.h>
函数原型
pid_t wait(int *status)
返回值
-
成功:退出的子进程的pid
-
失败:-1
处理子进程退出状态值的宏
-
WIFEXITED(status) :如果子进程正常退出,则该宏为真
-
WEXITSTATUS(status):如果子进程正常退出,则该宏获取子进程的退出值
进程的生老病死

核心状态说明
- 就绪态:进程已获得除 CPU 外的所有必要资源,等待系统调度器分配时间片。
- 运行态:进程正在 CPU 上执行指令。在单核心系统中,同一时间只有一个进程处于运行态。
- 睡眠态:进程因等待某事件(如 I/O 完成、sleep ()、信号量)而主动放弃 CPU,进入阻塞状态,直到事件发生后被唤醒。
- 暂停态:进程收到调试信号(如 SIGSTOP)后暂停执行,等待继续信号(如 SIGCONT)。
- 僵尸态:进程已经执行结束(调用 exit ()),但父进程尚未通过 wait ()/waitpid () 回收其退出状态和资源,此时进程描述符仍保留在系统中。
- 死亡态:父进程调用 wait () 回收了子进程的所有资源,进程彻底从系统中消失。

进程组、会话、终端
进程组:对相同类型的进程进行管理
进程组的诞生:
- 在shell里面直接执行一个应用程序,对于大部分进程来说,自己就是进程组的首进程**,进程组只有一个进程**
- 如果进程调用了fork函数,那么父子进程同属于一个进程组 ,父进程为首进程
- 在shell中通过管道执行连接起来的应用程序,两个程序同属于一个进程组,第一个程序为进程组的首进程
进程组ID(pgid),由首进程pid决定

会话
作用:管理进程组
会话的诞生
- 调用setsid函数,新建一个会话,应用程序作为会话的第一个进程,称为会话首进程
- 用户在终端正确登录后,启动shell时linux系统会创建一个新的会话,shell进程作为会话首进程
会话id:会话首进程id,SID
前台进程组
shell进程启动时,默认是前台进程组的首进程
前台进程组的首进程会占用会话所关联的终端来运行,shell启动其他应用程序时,其他程序成为首进程
先搞懂几个核心概念(用生活例子类比)
可以把 终端 想象成你家的客厅(唯一的活动空间),shell (比如 bash)就是客厅里的 "主人",进程组 是一群要占用客厅的人,前台进程组首进程 就是当前占用客厅的 "负责人",只有他能使用客厅的资源(比如沙发、电视)。
1. "shell 进程启动时,默认是前台进程组的首进程"
当你打开终端(比如 Linux 的 Terminal、macOS 的终端),系统会启动一个 shell 进程(比如 bash)。
- 此时这个 shell 就是 "客厅负责人"(前台进程组首进程),独占终端的输入输出:你输入
ls、cd这些命令,shell 能接收到;命令的执行结果,也能显示在终端上。 - 简单说:刚打开终端时,shell 是 "老大",终端完全听它的。
2. "前台进程组的首进程会占用会话所关联的终端来运行"
"占用终端" 就是指:这个进程能接收你在终端敲的键盘输入,也能把输出打印到终端屏幕上。
- 比如你没运行其他程序时,shell 作为首进程,你敲
echo hello,shell 接收到这个输入,执行后把hello输出到终端 ------ 这就是 "占用终端" 的体现; - 反之,如果是后台进程(比如
sleep 10 &),它不会占用终端,你敲键盘依然能被 shell 接收。
3. "shell 启动其他应用程序时,其他程序成为首进程"
当你在 shell 里输入并运行一个程序(比如 vim、python、ls),shell 会 "把客厅的控制权让出去":
- 原来的 shell 不再是前台进程组首进程,刚启动的这个程序 会变成新的前台进程组首进程,独占终端;
- 比如你输入
vim test.txt后:- shell 会暂停(不再接收你的键盘输入);
vim成为前台进程组首进程,占用终端:你敲的所有按键(比如编辑文字、按esc)都被vim接收,终端显示的也是vim的界面;- 当你退出
vim(按:wq),vim进程结束,shell 又会重新成为前台进程组首进程,终端的控制权回到 shell 手里,你又能输入 shell 命令了。
后台进程组
后台进程中的程序不占用终端
在shell进程里面启动程序时,加上&的符号可以指定程序运行在后台进程组中
ctrl+z 会使进程进入后台,同时停止执行
jobs:查看有哪些后台进程组

fg+job id 可以把后台进程组切换为前台进程组

终端
物理终端
- 串口终端
- lcd终端
伪终端
- ssh远程连接产生的终端
- 桌面系统启动的终端
虚拟终端
- Linux内核自带的,ctrl+Alt+f0-f6可以打开7个虚拟终端
守护进程
会话用来管理前后台进程组,会话一般关联着一个终端
当终端被关闭之后,会话中的所有进程都会被关掉
守护进程(Daemon)是脱离终端、在后台长期运行的进程(比如 Linux 里的 sshd、nginx 都是守护进程)。它的核心特点是:
- 不占用终端,即使启动它的终端关闭,进程依然运行;
- 不受终端信号(比如 Ctrl+C)影响;
- 在后台独立运行。
如何来写一个守护进程
1、创建一个子进程,父进程直接退出 (通过fork函数实现)
为什么要这么做?
- 脱离父进程的控制 :启动守护进程的初始进程(比如你在 shell 里敲命令启动)是前台进程组的一部分,父进程退出后,子进程会被操作系统的
init进程(PID=1)接管,不再依赖原启动终端; - 避免成为进程组首进程 :只有进程组首进程才能创建新会话(下一步要做的),但如果直接用原进程创建会话,它依然和终端关联;先 fork 出子进程,子进程不是进程组首进程,为下一步创建新会话做准备;
- 让 shell 认为命令执行完毕:父进程退出后,shell 会立刻返回(不会卡住),子进程则在后台继续运行。
2、创建一个新的会话
先理解:什么是 "会话(Session)"?
会话是一组进程组的集合,每个会话都和一个终端关联(比如你打开的 Terminal 窗口就是一个会话,关联着 shell 进程)。
- 会话有一个 "控制终端",会话里的前台进程组会占用这个终端;
- 当终端关闭时,内核会给会话里的所有进程发送
SIGHUP信号,导致进程退出(这就是为什么普通后台进程(加 &)终端关了也会退出)。
为什么要创建新会话?
setsid() 函数会做 3 件关键事,彻底让进程脱离原终端:
- 创建一个新的会话 ,子进程成为这个新会话的会话首进程;
- 创建一个新的进程组,子进程成为这个进程组的首进程;
- 脱离原会话的控制终端(新会话没有关联任何终端)。
这样一来,进程就不再受原终端的任何影响(终端关闭、Ctrl+C 都和它无关)。
3、改变守护进程的当前工作目录,改为"/"
为什么要这么做?
守护进程启动时,默认的当前工作目录是启动它的那个目录 (比如你在 /home/yourname 下运行守护进程,它的工作目录就是 /home/yourname)。如果不修改,会带来两个严重问题:
-
无法卸载挂载的文件系统 举个例子:如果你的守护进程工作目录是
/mnt/usb(U 盘挂载目录),只要这个守护进程还在运行,系统就无法卸载这个 U 盘(提示 "设备忙")。而根目录/是系统最顶层目录,永远不会被卸载,能避免这个问题。 -
原工作目录被删除 / 移动导致异常 如果守护进程的工作目录被手动删除(比如
/home/yourname被删),虽然进程不会立刻崩溃,但后续如果进程需要创建文件、访问相对路径,都会失败(比如想创建./log.txt,但当前目录已不存在)。
4、重设文件权限掩码
先搞懂:文件权限掩码(umask)是什么?
umask 是系统的 "权限过滤器" ,作用是限制新建文件 / 目录的默认权限。
- 新建文件的默认权限 =
0666 - umask(文件默认不能执行,所以是 666) - 新建目录的默认权限 =
0777 - umask(目录需要执行权限才能进入,所以是 777)
比如默认 umask 是 0022(常见的默认值):
- 新建文件权限:
0666 - 0022 = 0644(即-rw-r--r--) - 新建目录权限:
0777 - 0022 = 0755(即drwxr-xr-x)
为什么要重设守护进程的 umask?
守护进程继承了启动它的 shell 的 umask(比如 shell 的 umask 是 0077,会导致新建文件只有所有者能读写)。如果不重置:
- 权限不符合预期:比如守护进程想创建一个让其他用户也能读取的日志文件,但继承的 umask 会把权限限制死;
- 权限过严 / 过松:可能导致守护进程创建的文件无法被其他程序访问(过严),或权限太开放导致安全风险(过松)。
因此,守护进程通常会将 umask 设为 0(即取消所有权限限制),让进程手动控制新建文件 / 目录的权限,而不是依赖系统默认值。
5、关闭不需要的文件描述符

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#define MAXFILE 3
int main()
{
pid_t pid;
int fd, len, i, num;
char *buf = "the dameon is running\n";
len = strlen(buf) + 1;
/*1.创建子进程,销毁父进程*/
pid = fork();
if (pid < 0) {
printf("fork fail\n");
exit(1);
}
if (pid > 0) {
exit(0);
}
/*2.创建新会话,摆脱终端的影响*/
setsid();
/*3.改变当前工作目录*/
chdir("/");
/*4.重设文件掩码*/
umask(0);
/*5.关闭默认的文件描述符*/
for (i = 0; i < MAXFILE; i++) {
close(i);
}
/*6.实现守护进程的功能*/
while (1) {
fd = open("/var/log/dameon.log", O_CREAT | O_WRONLY | O_APPEND, 0666);
write(fd, buf, len);
close(fd);
sleep(5);
}
return 0;
}
普通进程伪装成守护进程
# 格式:nohup 你的程序命令 > 输出日志文件 2>&1 &
nohup ./your_program > /var/log/normal_daemon.log 2>&1 &
nohup(no hang up)的核心作用是忽略终端挂断信号(SIGHUP) ,配合 & 让进程在后台运行,从而模拟守护进程 "脱离终端、后台持续运行" 的核心特征。它和手动编写守护进程代码的区别在于:
nohup是 "用户态伪装",进程本质还是普通进程,只是不受终端退出影响;- 手动编写的守护进程是 "内核态改造",完全脱离终端会话,更符合标准守护进程规范。
| 特性 | nohup + &(伪装) | 手动编写守护进程(标准) |
|---|---|---|
| 脱离终端 | ✅(忽略 SIGHUP) | ✅(完全脱离会话) |
| 后台运行 | ✅ | ✅ |
| 占用终端进程组 | ✅(仍属于原进程组) | ❌(独立进程组) |
| 开机自启 | 需手动配置 | 可集成到系统服务(systemd) |
| 代码侵入性 | 0(无需改代码) | 需修改程序(fork/setsid 等) |
| 稳定性 | 较好(但可能被意外 kill) | 更高(完全独立) |
PS命令
ps(Process Status)是 Linux/Unix 系统中查看进程静态快照的核心命令,用于列出当前运行的进程、PID、状态、资源占用等信息。
静态快照 :只显示执行命令瞬间的进程状态,不实时刷新(区别于 top)。
# BSD 风格(推荐)
ps aux
# System V 风格
ps -ef
过滤指定进程(排查是否运行)
# 查找 nginx 进程
ps aux | grep nginx
# 查找 PID 为 1234 的进程
ps -p 1234
按资源排序(定位高占用)
# 按 CPU 使用率降序
ps aux --sort=-%cpu | head -10
# 按内存使用率降序
ps aux --sort=-%mem | head -10
自定义输出列(-o)
# 只显示 PID、用户、CPU、命令
ps -eo pid,user,%cpu,cmd
按用户 / 组筛选
# 查看 root 用户的所有进程
ps -u root
# 查看指定组的进程
ps -G wheel
查看线程信息
# 显示进程的线程(LWP)
ps -L -p 1234
ps aux 输出字段(关注进程本身):
USER:进程所有者PID:进程 ID(唯一标识)%CPU:CPU 使用率%MEM:内存使用率VSZ:虚拟内存大小(KB)RSS:物理内存大小(KB)TTY:终端设备(?表示无终端,后台进程)STAT:进程状态(见下文)START:启动时间TIME:累计 CPU 时间COMMAND:进程命令与参数
进程状态码(STAT 列)
R:Running(运行中)S:Sleeping(可中断睡眠,等待事件)D:Uninterruptible Sleep(不可中断,通常 I/O 等待)T:Stopped(停止,如 Ctrl+Z)Z:Zombie(僵尸进程,已结束但未被父进程回收)<:高优先级N:低优先级s:会话领导者l:多线程+:前台进程组
ps axjf(关注进程间的联系)
一个组合参数的 ps 命令,核心作用是以树状结构显示系统中所有进程的父子层级关系,非常适合排查进程之间的调用 / 派生关系。
直接执行即可看到完整的进程树:
ps axjf

核心输出字段(重点关注):
PPID:父进程 ID(可以看到谁创建了这个进程)PID:当前进程 IDPGID:进程组 IDSID:会话 IDTTY:进程关联的终端(?为后台进程)PGRP:进程组号UID:进程所属用户 IDCOMMAND:进程命令,树状结构中用├─/└─表示层级
僵尸进程和托孤进程
1. 进程正常退出流程
子进程退出时,会调用 exit() 函数,内核会释放其大部分资源,但会保留进程描述符(task_struct),其中包含退出状态等信息。父进程需要调用 wait() 或 waitpid() 等函数,来读取子进程的退出状态,并由内核彻底清理该子进程的所有资源。
2. 僵尸进程(Zombie Process)
- 产生原因 :子进程已经调用
exit()退出,但父进程没有调用wait()或waitpid()来回收它的退出状态和资源。 - 表现 :在
ps命令中,状态码为Z。此时子进程的进程描述符仍然存在,占用少量系统资源(如 PID),但无法被正常调度执行。 - 危害:大量僵尸进程会占用系统 PID 资源,导致无法创建新的进程。
- 处理方式 :
- 终止父进程:当父进程退出后,init 进程(PID=1)会接管这些僵尸进程,并自动调用
wait()清理它们。 - 让父进程显式调用
wait()或waitpid(),或通过信号处理函数(如捕获SIGCHLD)来自动回收。
- 终止父进程:当父进程退出后,init 进程(PID=1)会接管这些僵尸进程,并自动调用
3. 托孤进程(孤儿进程,Orphan Process)
- 产生原因:父进程先于子进程退出,子进程就变成了 "孤儿"。
- 处理机制 :内核会将孤儿进程的父进程 "过继" 给 init 进程(PID=1),由 init 进程负责在它们退出时调用
wait()回收资源,因此孤儿进程不会变成僵尸进程。
| 特性 | 僵尸进程 (Zombie) | 孤儿进程 (Orphan) |
|---|---|---|
| 谁先退出 | 子进程先退出 | 父进程先退出 |
| 状态码 | Z |
正常运行状态(如 S, R) |
| 资源占用 | 占用 PID 等少量资源 | 正常占用资源 |
| 危害 | 大量存在会耗尽 PID 资源 | 无直接危害,由 init 进程接管 |
| 解决方法 | 终止父进程或让父进程调用wait() |
由 init 进程自动回收,无需干预 |
子进程 "退出" 的两层含义
子进程调用 exit() 后,它的退出分为两个阶段:
-
用户态退出:
- 子进程的代码停止执行,用户空间的内存、文件描述符等资源被释放。
- 从用户的角度看,这个程序 "已经结束了",不再做任何事情。
-
内核态残留:
- 内核中仍然保留着该进程的进程描述符(task_struct),里面记录了它的 PID、退出状态码、资源使用统计等信息。
- 这些信息是留给父进程的 "遗产",父进程需要通过
wait()来 "认领" 这份遗产,内核才会彻底删除这个进程描述符。
1. 为什么会变成 "僵尸"?
- 子进程退出后,内核会向父进程发送一个
SIGCHLD信号,通知它 "你的孩子走了,快来收尸"。 - 如果父进程没有 处理这个信号,也没有主动调用
wait()或waitpid(),内核就会一直保留那个进程描述符,这个进程就变成了僵尸进程(Zombie Process)。 - 它的状态码是
Z,表示它已经死了,但 "尸体" 还在系统里占着位置。
2. 僵尸进程的危害
虽然僵尸进程不占用 CPU 和大量内存,但它会:
- 占用一个唯一的 PID 号。
- 如果系统中产生了大量僵尸进程,会耗尽可用的 PID 资源,导致无法创建新的进程。
3. 如何 "收尸"?
- 父进程主动回收 :在代码中调用
wait()或waitpid(),或者注册信号处理函数来捕获SIGCHLD并在处理函数中回收。 - 终止父进程 :如果父进程退出,init 进程(PID=1)会自动接管所有僵尸子进程,并调用
wait()清理它们。
进程间通信
IPC 是操作系统提供的机制,用于让独立的进程之间交换数据、同步行为或传递控制信号,主要解决以下四类问题:
- 数据传输:在进程之间传递字节流或消息块,例如管道、消息队列。
- 资源共享:让多个进程访问同一块内存或文件,例如共享内存。
- 事件通知:一个进程向另一个或一组进程发送信号,告知特定事件发生,例如信号(Signal)。
- 进程控制:一个进程完全控制另一个进程的执行(如调试、挂起、恢复),例如通过调试器或 ptrace。
Linux 系统下的 IPC 分类与特点
1. 早期 Unix 系统 IPC
- 管道(Pipe):半双工的单向通信通道,只能在具有亲缘关系的进程(如父子进程)间使用。
- 信号(Signal):用于通知进程发生了异步事件,是一种 "软中断" 机制,不适合传输大量数据。
- FIFO(命名管道):是管道的一种扩展,允许无亲缘关系的进程间通信,在文件系统中以特殊文件形式存在。
2. System V IPC(贝尔实验室)
这是一套经典的 IPC 机制,主要用于本地进程间通信:
- System V 消息队列:消息以链表形式在内核中维护,支持按类型筛选消息,比管道更灵活。
- System V 信号量:用于实现进程间的同步与互斥,防止多个进程同时访问临界资源。
- System V 共享内存 :允许多个进程共享同一块物理内存,是所有 IPC 中速度最快的方式,因为数据无需在用户态和内核态之间拷贝。
3. Socket IPC(BSD)
- 套接字(Socket):起源于 BSD Unix,是一种通用的 IPC 接口。它不仅支持本地进程间通信(Unix 域套接字),还支持跨网络的主机间通信(网络套接字),是网络编程的基础。
4. POSIX IPC(IEEE)
为了统一不同 Unix 系统的 IPC 接口,IEEE 制定了 POSIX 标准,提供了与 System V 功能类似但接口更现代、更易用的实现:
- POSIX 消息队列:接口更简洁,支持消息优先级,并且可以与文件系统事件(如 select/poll)集成。
- POSIX 信号量:分为命名信号量和无名信号量,无名信号量可用于线程间同步。
- POSIX 共享内存 :通过
mmap()系统调用将文件或设备映射到进程地址空间,使用更灵活。
💡 选型建议
- 追求速度 :优先选择 共享内存(无论是 System V 还是 POSIX)。
- 需要跨网络 :使用 Socket。
- 简单的父子进程通信 :使用 管道 最方便。
- 事件通知 :使用 信号。
无名管道
一、pipe 函数核心信息
- 头文件 :
#include <unistd.h> - 函数原型 :
int pipe(int pipefd[2]); - 返回值 :
- 成功:返回
0,并在pipefd数组中填充两个文件描述符:pipefd[0]:管道的读端pipefd[1]:管道的写端
- 失败:返回
-1,并设置errno
- 成功:返回
二、无名管道的特点
- 特殊文件 :它是内核中的一块缓冲区,没有名字,因此无法用
open()创建,但必须用close()关闭。 - 单向通信 :数据只能从写端(
pipefd[1])流向读端(pipefd[0])。 - 亲缘限制 :只能在具有共同祖先的进程(如父子、兄弟进程)间使用,因为文件描述符需要通过
fork()继承。 - 阻塞特性 :
- 当管道为空时,
read()操作会阻塞,直到有数据写入。 - 当管道满时,
write()操作会阻塞,直到有数据被读出。
- 当管道为空时,
- 生命周期:当所有指向管道的文件描述符都被关闭后,管道才会被内核销毁。
三、标准使用步骤(父进程 → 子进程通信)
-
父进程创建管道 :调用
pipe(),得到读写两端的文件描述符。 -
父进程 fork 子进程:子进程会继承父进程的这两个文件描述符。
-
关闭无用端口 :
- 若父进程要写、子进程要读:父进程关闭
pipefd[0],子进程关闭pipefd[1]。 - 若父进程要读、子进程要写:父进程关闭
pipefd[1],子进程关闭pipefd[0]。
- 若父进程要写、子进程要读:父进程关闭
-
进行读写操作 :使用
write()和read()在两端传输数据。 -
关闭读写端口:通信结束后,关闭各自持有的文件描述符。
示例代码(父写子读)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { // 子进程 close(pipefd[1]); // 关闭写端 char buf[1024]; ssize_t n = read(pipefd[0], buf, sizeof(buf)-1); if (n == -1) { perror("read"); exit(EXIT_FAILURE); } buf[n] = '\0'; printf("子进程收到: %s\n", buf); close(pipefd[0]); exit(EXIT_SUCCESS); } else { // 父进程 close(pipefd[0]); // 关闭读端 const char *msg = "Hello from parent!"; if (write(pipefd[1], msg, strlen(msg)) == -1) { perror("write"); exit(EXIT_FAILURE); } close(pipefd[1]); wait(NULL); // 等待子进程结束 exit(EXIT_SUCCESS); }}
有名管道
一、mkfifo 函数核心信息
-
头文件 :
#include <sys/types.h> #include <sys/stat.h> -
函数原型 :
int mkfifo(const char *filename, mode_t mode);filename:要创建的有名管道文件路径(如/tmp/myfifo)。mode:文件权限(如0666),最终权限会受系统umask影响。
-
返回值 :
- 成功:返回
0,在文件系统中创建一个特殊的管道文件。 - 失败:返回
-1,并设置errno。
- 成功:返回
二、有名管道的特点
- 有文件名:在文件系统中以特殊文件形式存在,因此可以被任意进程通过路径名打开,突破了无名管道只能在亲缘进程间使用的限制。
- 任意进程通信:只要知道管道文件的路径,无亲缘关系的进程也可以通过它进行数据传输。
- 单向通信:本质上仍是半双工的,数据单向流动。若要双向通信,需要创建两个 FIFO。
- 阻塞特性 :
- 当管道为空时,
read()操作会阻塞,直到有数据写入。 - 当管道满时,
write()操作会阻塞,直到有数据被读出。 write操作具有原子性,保证了多进程写入时数据不会被交错。一个write操作要么完整地把数据写入管道,要么完全不写入,不会出现 "写了一半" 的情况。
- 当管道为空时,
- 生命周期:管道文件本身是持久化的,除非被手动删除,否则会一直存在于文件系统中,即使所有进程都关闭了它。
三、标准使用步骤
-
创建管道 :一个进程调用
mkfifo()创建有名管道文件。 -
打开管道 :
- 写进程以
O_WRONLY模式打开管道。 - 读进程以
O_RDONLY模式打开管道。 - 注意:
open()操作会阻塞,直到另一端也打开了管道。
- 写进程以
-
数据传输 :使用
write()和read()进行读写。 -
关闭管道:通信结束后,关闭文件描述符。
-
删除管道 :不再需要时,使用
unlink()删除文件系统中的管道文件。写进程(writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int main() {
const char *fifo_path = "/tmp/myfifo";// 创建有名管道,如果已存在则忽略 if (mkfifo(fifo_path, 0666) == -1) { perror("mkfifo"); // 如果错误是因为文件已存在,可以继续执行 } int fd = open(fifo_path, O_WRONLY); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); } const char *msg = "Hello from FIFO writer!"; if (write(fd, msg, sizeof(msg)) == -1) { perror("write"); exit(EXIT_FAILURE); } close(fd); return 0;}
读进程(reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>int main() {
const char *fifo_path = "/tmp/myfifo";int fd = open(fifo_path, O_RDONLY); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); } char buf[1024]; ssize_t n = read(fd, buf, sizeof(buf)-1); if (n == -1) { perror("read"); exit(EXIT_FAILURE); } buf[n] = '\0'; printf("收到: %s\n", buf); close(fd); unlink(fifo_path); // 删除管道文件 return 0;}
信号
一、信号的产生信号(Signal)是软件或硬件触发的异步通知,用于告知进程发生了特定事件。
硬件触发
- 执行非法指令:如除零操作、未定义指令等。
- 访问非法内存:如访问空指针、越界内存地址。
- 驱动程序:如硬件设备(键盘、定时器)通过中断触发信号。
软件触发
- 控制台上的快捷键:
Ctrl+C:发送中断信号(SIGINT),用于终止前台进程。Ctrl+\\:发送退出信号(SIGQUIT),用于终止进程并生成核心转储。Ctrl+Z:发送停止信号(SIGTSTP),用于暂停前台进程。
kill命令:向指定进程发送任意信号。- 程序调用
kill()函数:在代码中主动向自身或其他进程发送信号。
二、信号的处理方式
当进程接收到信号时,有三种主要的处理方式:
忽略(Ignore)
- 进程告知操作系统,当该信号发生时,不做任何处理,就像信号从未出现过一样。
- 注意:SIGKILL(终止进程)和 SIGSTOP(暂停进程)这两个信号无法被忽略。
捕获并处理(Catch and Handle)
- 进程注册一个自定义的信号处理函数。
- 当信号发生时,操作系统会暂停进程的正常执行,转而执行这个自定义函数,处理完成后再恢复原执行流程。
默认(Default)
- 进程不做任何特殊设置,使用系统为每种信号预设的默认行为。
- 常见的默认行为包括:终止进程、忽略信号、暂停进程、生成核心转储等。
常用信号分析表解读
| 信号名 | 信号编号 | 产生原因 | 默认处理方式 |
|---|---|---|---|
| SIGHUP | 1 | 关闭终端 | 终止 |
| SIGINT | 2 | Ctrl+C |
终止 |
| SIGQUIT | 3 | Ctrl+\ |
终止 + 转储 |
| SIGABRT | 6 | abort() |
终止 + 转储 |
| SIGFPE | 8 | 算术错误 | 终止 |
| SIGKILL | 9 | kill -9 pid |
终止,不可捕获 / 忽略 |
| SIGUSR1 | 10 | 自定义 | 忽略 |
| SIGSEGV | 11 | 段错误 | 终止 + 转储 |
| SIGUSR2 | 12 | 自定义 | 忽略 |
| SIGALRM | 14 | alarm() |
终止 |
| SIGTERM | 15 | kill pid |
终止 |
| SIGCHLD | 17 | (子) 状态变化 | 忽略 |
| SIGSTOP | 19 | Ctrl+Z |
暂停,不可捕获 / 忽略 |
signal、kill、raise 三个函数
1. signal 函数
作用 :用于为指定信号设置自定义的处理函数,是最基础的信号注册接口。
-
头文件 :
#include <signal.h> -
函数原型 :
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); -
参数说明 :
signum:要设置的信号编号,如SIGINT、SIGUSR1等。handler:信号处理方式,有三种选择:SIG_IGN:忽略该信号。SIG_DFL:使用系统默认处理方式。- 自定义函数指针:指向
void (*)(int)类型的函数,作为自定义处理逻辑。
-
返回值 :
-
成功:返回上一次设置的 handler。
-
失败:返回
SIG_ERR。#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>/* 信号处理函数 */
void signal_handler(int sig)
{
printf("\nthis signal number is %d \n", sig);if (sig == SIGINT) { printf("I have get SIGINT!\n"); printf("The signal has been restored to the default processing mode!\n"); /* 恢复信号为默认处理 */ signal(SIGINT, SIG_DFL); }}
int main(void)
{
printf("\nthis is an alarm test function\n");/* 设置信号处理的回调函数 */ signal(SIGINT, signal_handler); while (1) { printf("waiting for the SIGINT signal, please enter \"ctrl + c\"...\n"); sleep(1); } exit(0);}
-
2. kill 函数
作用:向指定进程(或进程组)发送一个信号,常用于进程间通信或终止其他进程。
-
头文件 :
#include <signal.h>、#include <sys/types.h> -
函数原型 :
int kill(pid_t pid, int signum); -
参数说明 :
pid:目标进程 ID。特殊值:pid > 0:发送给进程 ID 为pid的进程。pid = 0:发送给与当前进程同组的所有进程。pid = -1:发送给系统中除 init 进程外的所有进程(需权限)。pid < -1:发送给进程组 ID 为-pid的所有进程。
signum:要发送的信号编号。
-
返回值 :成功返回 0,失败返回 -1 并设置
errno。#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main(void)
{
pid_t pid;/* 创建子进程 */ if ((pid = fork()) < 0) { printf("Fork error!\n"); exit(1); } if (pid == 0) { /* 子进程中使用 raise 函数发送 SIGSTOP 信号,使子进程暂停 */ printf("Child is waiting for SIGSTOP signal!\n"); /* 子进程停在这里 */ raise(SIGSTOP); /* 子进程没有机会运行到这里 */ printf("Child won't run here forever!\n"); exit(0); } else { /* 睡眠5秒,让子进程先执行并暂停 */ sleep(5); /* 发送 SIGKILL 信号杀死子进程 */ if (kill(pid, SIGKILL) < 0) { printf("Parent kill %d failed!\n", pid); } /* 一直阻塞直到子进程退出(杀死) */ wait(NULL); /* 父进程退出运行 */ printf("Parent exit!\n"); exit(0); }}
3. raise 函数
作用 :向当前进程自身发送一个信号,等价于 kill(getpid(), signum)。
-
头文件 :
#include <signal.h> -
函数原型 :
int raise(int signum); -
参数说明 :
signum:要发送的信号编号。
-
返回值:成功返回 0,失败返回非 0 值。
核心应用场景
- signal :在程序启动时注册信号处理函数,用于优雅地处理
Ctrl+C、程序退出等事件。 - kill :在父进程中向子进程发送控制信号,或在命令行中使用
kill命令管理进程。 - raise:在程序内部主动触发信号,模拟外部事件,或触发自身的信号处理逻辑。
信号集处理函数
一、核心概念
-
屏蔽信号集(Signal Mask / Blocked Set)
- 也叫 "阻塞信号集",是当前进程中被阻塞、暂时不会被递送的信号集合。
- 当一个信号被加入屏蔽集后,即使它产生了,也不会被进程处理,直到它被从屏蔽集中移除。
-
未处理信号集(Pending Set)
- 也叫 "待处理信号集",记录了已经产生但因为被屏蔽而尚未被处理的信号。
- 一旦信号从屏蔽集中移除,未处理信号集中对应的信号就会被递送并处理。
- 非实时信号(1-31) :不排队,只保留一个。这意味着如果同一个非实时信号(如
SIGINT)在被屏蔽期间多次触发,进程只会收到一次通知。 - 实时信号(34-64) :排队,保留全部。
- 实时信号支持可靠排队,多次触发会按顺序 被记录和递送,不会丢失。
| 函数 / 宏 | 作用 |
|---|---|
sigemptyset(sigset_t *set) |
将信号集合初始化为空集 |
sigfillset(sigset_t *set) |
将信号集合初始化为包含所有信号 |
sigaddset(sigset_t *set, int signum) |
将指定信号 signum 添加到集合 set 中 |
sigdelset(sigset_t *set, int signum) |
将指定信号 signum 从集合 set 中移除 |
sigismember(const sigset_t *set, int signum) |
判断信号 signum 是否在集合 set 中,是则返回 1 |
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset) |
检查或修改进程的信号屏蔽字(屏蔽信号集) |

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void my_func(int signo)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("hello\n");
sleep(5);
printf("world\n");
}
int main()
{
signal(SIGINT, my_func);
while (1);
return 0;
}
上述函数的执行过程解析
-
主程序初始化
main函数中,通过signal(SIGINT, my_func)注册了对SIGINT(Ctrl+C)信号的自定义处理函数my_func。- 程序进入死循环
while(1);,等待信号触发。
-
第一次按下
Ctrl+C- 触发
SIGINT信号,主程序被打断,执行my_func。 - 在
my_func中:- 初始化一个信号集
set,并将SIGINT加入其中。 - 调用
sigprocmask(SIG_UNBLOCK, &set, NULL),解除对SIGINT的阻塞。 - 打印
hello。 - 调用
sleep(5),使进程休眠 5 秒。
- 初始化一个信号集
- 关键点 :在
sleep(5)期间,进程对SIGINT信号不再阻塞。
- 触发
-
在
sleep(5)期间再次按下Ctrl+C- 再次触发
SIGINT信号。由于此时信号未被阻塞,my_func会被再次递归调用。 - 第二次执行
my_func:- 再次解除对
SIGINT的阻塞。 - 打印第二个
hello。 - 再次进入
sleep(5)。
- 再次解除对
- 再次触发
-
休眠结束
- 第二次
sleep(5)结束,打印第二个world,第二次my_func调用返回。 - 第一次
sleep(5)结束,打印第一个world,第一次my_func调用返回。 - 程序回到死循环,继续等待。
- 第二次
SIGINT 信号本身不是 "天生阻塞" 的 ,它的阻塞状态完全由进程的信号屏蔽集(Signal Mask)决定,默认情况下(进程未做任何信号屏蔽操作),SIGINT 是未被阻塞的。
1. 信号的 "默认阻塞状态"
Linux 系统中,所有信号(包括 SIGINT)的默认状态都是:
- 不在进程的屏蔽信号集(Blocked Set)中 → 未被阻塞
- 未出现在未处理信号集(Pending Set)中 → 无待处理信号
这意味着:默认情况下,你按下 Ctrl+C 发送 SIGINT,进程会立即响应(执行默认行为:终止进程,或执行你注册的自定义处理函数)。
2. 信号处理时的 "自动阻塞"(核心易错点)
当你为 SIGINT 注册了自定义处理函数后,在处理函数执行期间 ,内核会自动将 SIGINT 加入进程的屏蔽集------ 这是 Linux 的默认保护机制,目的是防止信号处理函数被递归调用。
system-V 消息队列
system-V ipc特点
-
独立于进程
-
没有文件名和文件描述符
-
IPC对象具有key和ID
消息队列用法
-
定义一个唯一key(ftok)
-
构造消息对象(msgget)
-
发送特定类型消息(msgsnd)
-
接受特定类型消息(msgrcv)
-
删除消息队列(msgctl)
ftok函数
功能:获取一个key
函数原型:
key_t ftok(const char *path,int proj_id)
参数:
-
path:一个合法路径
-
proj_id:一个整数
返回值:
成功:合法键值
失败:-1
msgget函数
功能:获取消息队列ID
函数原型:
int msgget(key_t key,int msgflg)
参数:
-
key:消息队列的键值
-
msgflg:
-
IPC_CREAT:如果消息队列不存在,则创建
-
mode:访问权限
-
返回值:
成功:该消息队列的ID
失败:-1
msgsnd函数
功能:发送消息到消息队列
函数原型:
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
参数:
-
msqid:消息队列ID
-
msgp:消息缓存区
-
struct msgbuf
{
long mtype; //消息标识
char mtext[1]; //消息内容
}
-
-
msgsz:消息正文的字节数
-
msgflg:
-
IPC_NOWAIT:非阻塞发送
-
0:阻塞发送
-
返回值:
成功:0
失败:-1
msgrcv函数
功能:从消息队列读取消息
函数原型:
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg)
参数:
-
msqid:消息队列ID
-
msgp:消息缓存区
-
msgsz:消息正文的字节数
-
msgtyp:要接受消息的标识
-
msgflg:
-
IPC_NOWAIT:非阻塞读取
-
MSG_NOERROR:截断消息
-
0:阻塞读取
-
返回值:
成功:0
失败:-1
msgctl函数
功能:设置或获取消息队列的相关属性
函数原型:
int msgctl(int msgqid,int cmd,struct maqid_ds *buf)
-
msgqid:消息队列的ID
-
cmd
-
IPC_STAT:获取消息队列的属性信息
-
IPC_SET:设置消息队列的属性
-
IPC_RMID:删除消息队列
-
-
buf:相关结构体缓冲区
消息发送端(msg_send.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>// 定义消息结构体(需与接收端一致)
struct msgbuf {
long mtype; // 消息类型(≥1)
char mtext[1024]; // 消息内容
};int main() {
key_t key;
int msgid;
struct msgbuf msg;// 1. 生成唯一 key key = ftok("./test_ipc", 100); // 需确保 ./test_ipc 文件存在 if (key == -1) { perror("ftok failed"); exit(1); } // 2. 创建/获取消息队列 msgid = msgget(key, IPC_CREAT | 0666); if (msgid == -1) { perror("msgget failed"); exit(1); } // 3. 构造并发送消息 msg.mtype = 1; // 消息类型设为 1 strcpy(msg.mtext, "Hello, System-V Message Queue!"); // 阻塞发送消息(msgflg=0) if (msgsnd(msgid, &msg, strlen(msg.mtext), 0) == -1) { perror("msgsnd failed"); exit(1); } printf("发送消息成功:%s\n", msg.mtext); return 0;}
消息接收端(msg_recv.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/ipc.h>// 与发送端一致的消息结构体
struct msgbuf {
long mtype;
char mtext[1024];
};int main() {
key_t key;
int msgid;
struct msgbuf msg;
ssize_t recv_len;// 1. 生成与发送端相同的 key key = ftok("./test_ipc", 100); if (key == -1) { perror("ftok failed"); exit(1); } // 2. 获取消息队列(无需创建,直接获取) msgid = msgget(key, 0666); if (msgid == -1) { perror("msgget failed"); exit(1); } // 3. 接收类型为 1 的消息(阻塞接收) recv_len = msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0); if (recv_len == -1) { perror("msgrcv failed"); exit(1); } // 手动补全字符串结束符 msg.mtext[recv_len] = '\0'; printf("接收消息成功:%s(长度:%zd 字节)\n", msg.mtext, recv_len); // 4. 删除消息队列(仅需一端执行) if (msgctl(msgid, IPC_RMID, NULL) == -1) { perror("msgctl remove failed"); exit(1); } printf("消息队列已删除\n"); return 0;}
system-V 信号量
本质
计数器
作用
保护共享资源
-
互斥
-
同步
信号量用法
-
定义一个唯一key(ftok)
-
构造一个信号量(semget)
-
初始化信号量(semctl SETVA)
-
对信号量进行P/V操作(semop)
-
删除信号量(semctl RMID)
重要概念补充
- P 操作(减 1) :
sem_op = -1,申请资源,若信号量值为 0 则阻塞(直到值 > 0)。 - V 操作(加 1) :
sem_op = 1,释放资源,唤醒等待该信号量的进程。 - SEM_UNDO 标志:进程异常退出时,内核会自动撤销该进程对信号量的操作,避免信号量值异常(必加!)。
- 信号量初始化 :
semctl的SETVAL仅需初始化一次,通常由第一个创建信号量的进程执行。 - nsems 参数 :
semget的nsems表示创建的信号量集中包含的信号量个数,互斥场景下设为 1 即可。
semget函数
功能:获取信号量的ID
函数原型:
int semget(key_t key,int nsems,int semflg)
参数:
-
key:信号量键值
-
nsems:信号量数量
-
semflg:
-
IPC_CREATE:信号量不存在则创建
-
mode:信号量的权限
-
返回值:
成功:信号量ID
失败:-1
semctl函数
功能:获取或设置信号量的相关属性
函数原型:
int semctl(int semid,int semnum,int cmd,union semun arg)
参数:
-
semid:信号量ID
-
semnum:信号量编号
-
cmd:
-
IPC_STAT:获取信号量的属性信息
-
IPC_SET:设置信号量的属性
-
IPC_RMID:删除信号量
-
IPC_SETVAL:设置信号量的值
-
-
arg:
union semun { int val; struct semid_ds *buf; }
返回值:
成功:由cmd类型决定
失败:-1
semop函数
函数原型:
int semop(int semid,struct sembuf *sops,size_t nsops)
参数:
-
semid:信号量ID
-
sops:信号量操作结构体数组
struct sembuf
{
short sem_num; //信号量编号
short sem_op;//信号量P/V操作
short sem_flg; //信号量行为,SEM_UNDO
}
- nsops:信号量数量
返回值:
成功:0
失败:-1
头文件 sem.h(信号量操作封装)
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
};
/* 初始化信号量 */
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
{
printf("Initialize semaphore");
return -1;
}
return 0;
}
/* 删除信号量 */
int del_sem(int sem_id)
{
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
{
perror("Delete semaphore");
return -1;
}
}
/* P 操作 */
int sem_p(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0; /* 单个信号量的编号为 0 */
sops.sem_op = -1; /* 表示 P 操作(申请资源)*/
sops.sem_flg = SEM_UNDO; /* 系统自动释放残留信号量 */
if (semop(sem_id, &sops, 1) == -1)
{
perror("P operation");
return -1;
}
return 0;
}
/* V 操作 */
int sem_v(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0; /* 单个信号量的编号为 0 */
sops.sem_op = 1; /* 表示 V 操作(释放资源)*/
sops.sem_flg = SEM_UNDO; /* 系统自动释放残留信号量 */
if (semop(sem_id, &sops, 1) == -1)
{
perror("V operation");
return -1;
}
return 0;
}
主程序 main.c(进程同步示例)
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
#define DELAY_TIME 3
int main(void)
{
pid_t result;
int sem_id;
/* 创建一个信号量集(包含1个信号量) */
sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
if (sem_id == -1) {
perror("semget failed");
exit(1);
}
/* 初始化信号量值为 0 */
init_sem(sem_id, 0);
/* 调用 fork() 创建子进程 */
result = fork();
if (result == -1)
{
perror("Fork\n");
exit(1);
}
else if (result == 0) /* 子进程 */
{
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
printf("The child process is running...\r\n");
sem_v(sem_id); // 子进程执行完后,执行V操作,释放信号量
}
else /* 父进程 */
{
sem_p(sem_id); // 父进程先执行P操作,阻塞等待子进程释放信号量
printf("The father process is running...\r\n");
sem_v(sem_id); // 父进程执行完后,执行V操作
del_sem(sem_id); // 删除信号量
}
exit(0);
}
这是一个典型的进程同步示例,通过信号量实现 "子进程先执行,父进程后执行" 的顺序控制:
-
信号量初始化:
- 信号量初始值设为
0,这意味着父进程执行sem_p时会立即阻塞,因为信号量值为 0,无法申请到资源。
- 信号量初始值设为
-
子进程行为:
- 子进程先睡眠 3 秒,模拟耗时操作。
- 睡眠结束后,打印提示信息,然后执行
sem_v,将信号量值从0增加到1,释放资源。
-
父进程行为:
- 父进程一开始就执行
sem_p,由于信号量初始值为0,父进程会阻塞等待。 - 当子进程执行
sem_v后,信号量值变为1,父进程的sem_p操作成功,解除阻塞,开始执行自己的代码。 - 父进程执行完后,再次执行
sem_v恢复信号量,并调用del_sem删除信号量集。
- 父进程一开始就执行
system-V 共享内存
作用
高效率传输大量数据

共享内存用法
-
定义一个唯一key(ftok)
-
构造一个共享内存对象(shmget)
-
共享内存映射(shmat)
-
解除共享内存映射(shmdt)
-
删除共享内存(shmctl RMID)
shmget函数
功能:获取共享内存对象的ID
函数原型:
int shmget(key_t key,int size,int shmflg)
参数:
-
key:共享对象键值
-
size:共享内存大小
-
shmflg:
-
IPC_CREATE:共享内存不存在则创建
-
mode:共享内存的权限
-
返回值:
成功:共享内存ID
失败:-1
shmat函数
功能:映射共享内存
函数原型:
int shmat(int shmid,const void *shmaddr,int shmflg)
参数:
-
shmid:共享内存ID
-
shmaddr:映射地址,NULL为自动分配
-
shmflg:
-
SHM_RDONLY:只读方式映射
-
0:可读可写
-
返回值:
成功:共享内存首地址
失败:-1
shmdt函数
功能:解除共享内存映射
函数原型:
int shmdt(const void *shmaddr)
参数:
shmaddr:映射地址
返回值:
成功:0
失败:-1
shmctl函数
功能:获取或设置共享内存的相关属性
函数原型:
int shmctl(int shmid,int cmd,struct shmid_ds *buf)
参数:
-
shmid:共享内存ID
-
cmd:
-
IPC_STAT:获取共享内存的属性信息
-
IPC_SET:设置共享内存的属性
-
IPC_RMID:删除共享内存
-
-
buf:属性缓冲区
返回值:
成功:由cmd类型决定
失败:-1
System-V 信号量 + 共享内存 的组合使用场景,实现了父子进程间的同步通信。
核心逻辑是:子进程向共享内存写入数据,父进程在信号量同步下读取数据。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <sys/wait.h>
// 必须手动声明的 semun 联合体
union semun {
int val;
struct semid_ds *buf;
};
// 信号量操作函数(复用之前的封装)
int init_sem(int sem_id, int init_value) {
union semun sem_union;
sem_union.val = init_value;
if (semctl(sem_id, 0, SETVAL, sem_union) == -1) {
perror("init_sem failed");
return -1;
}
return 0;
}
int sem_p(int sem_id) {
struct sembuf sops = {0, -1, SEM_UNDO};
if (semop(sem_id, &sops, 1) == -1) {
perror("sem_p failed");
return -1;
}
return 0;
}
int sem_v(int sem_id) {
struct sembuf sops = {0, 1, SEM_UNDO};
if (semop(sem_id, &sops, 1) == -1) {
perror("sem_v failed");
return -1;
}
return 0;
}
int del_sem(int sem_id) {
union semun sem_union;
if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) {
perror("del_sem failed");
return -1;
}
return 0;
}
#define DELAY_TIME 3
int main(void) {
pid_t result;
int sem_id;
int shm_id;
char* addr; // 共享内存映射地址
// 1. 创建信号量集(key=6666,1个信号量,权限0666)
sem_id = semget((key_t)6666, 1, 0666 | IPC_CREAT);
if (sem_id == -1) {
perror("semget failed");
exit(1);
}
// 2. 创建共享内存(key=7777,大小1024字节,权限0666)
shm_id = shmget((key_t)7777, 1024, 0666 | IPC_CREAT);
if (shm_id == -1) {
perror("shmget failed");
exit(1);
}
// 3. 初始化信号量为0(实现同步:父进程先阻塞)
init_sem(sem_id, 0);
// 4. 创建子进程
result = fork();
if (result == -1) {
perror("Fork failed");
exit(1);
}
else if (result == 0) { // 子进程
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
// 映射共享内存到子进程地址空间
addr = shmat(shm_id, NULL, 0);
if (addr == (void*)-1) {
printf("shmat111 error!\n");
exit(-1);
}
// 向共享内存写入数据("helloworld"含结束符共11字节)
memcpy(addr, "helloworld", 11);
printf("The child process is running...\r\n");
// V操作:释放信号量,唤醒父进程
sem_v(sem_id);
// 子进程解除共享内存映射(图片中缺失,需补充)
shmdt(addr);
exit(0);
}
else { // 父进程
// P操作:阻塞,等待子进程V操作唤醒
sem_p(sem_id);
printf("The father process is running...\r\n");
// 映射共享内存到父进程地址空间
addr = shmat(shm_id, NULL, 0);
if (addr == (void*)-1) {
printf("shmat222 error!\n");
exit(-1);
}
// 读取并打印共享内存中的数据
printf("shared memory string:%s\r\n", addr);
// 解除共享内存映射
shmdt(addr);
// 等待子进程退出(避免僵尸进程)
wait(NULL);
// 删除信号量和共享内存(图片中缺失,需补充)
del_sem(sem_id);
shmctl(shm_id, IPC_RMID, NULL);
exit(0);
}
}
