Linux多进程开发
类型标识:每个进程由进程号(PID)标识,类型为取值范围:进程号范围是0~32767,操作系统中的进程数量是固定的唯一性保证:进程号总是唯一的,但可以重用(终止后的进程号可被新进程使用)Linux进程——exec族函数、exec族函数与fork函数的配合-CSDN博客exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
进程
Linux系统__xzls的博客-CSDN博客
这个专栏写的很不错
1. 基本概念
程序
程序是包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程:
- 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释
- 文件中的其他信息。(ELF可执行连接格式)
- 机器语言指令:对程序算法进行编码。
- 程序入口地址:标识程序开始执行时的起始指令位置。
- 数据:程序文件包含的变量初始值和程序使用的字面量值(比如字符串)。
- 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和
- 运行时的符号解析(动态链接)。
- 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及
- 加载共享库的动态连接器的路径名。
- 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。
进程
- 进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活
- 动。
- 它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的
- 执行单元。
- 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的
- 各项系统资源。
- 从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序
- 代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信
- 息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的
- 有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。
- 对于一个单 CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始
- 运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个。
- 在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级,1秒可以执行大约
- 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
单道多道程序设计
- 单道程序,即在计算机内存中只允许一个的程序运行。
- 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。引入多道程序设计技术的根本目的是为了提高 CPU 的利用率。
- 对于一个单 CPU 系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU 上运行的程序只有一个。
- 在多道程序设计模型中,多个进程轮流使用 CPU。而当下常见 CPU 为纳秒级,1秒可以执行大约10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
并行和并发



进程控制块(PCB)
为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个
PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是
task_struct 结构体。
在 /usr/src/linux-headers-xxx/include/linux/sched.h 文件中可以查看 struct task_struct 结构体定义。
其内部成员有很多,我们只需要掌握以下部分即可:
- 进程id:系统中每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
- 进程的状态:有就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录(Current Working Directory)
- umask 掩码
- 文件描述符表,包含很多指向 file 结构体的指针
- 和信号相关的信息
- 用户 id 和组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
2. 进程的状态


-
新建态:
- 出现时机:进程刚被创建时
- 资源状态:尚未分配必要资源,未加入就绪队列
- 转换方向:资源分配完成后转为就绪态
-
终止态:
- 进入条件:
- 正常结束(执行完所有指令)
- 异常终止(不可克服错误)
- 被强制终止(操作系统或其他进程干预)
- 后续处理:
- 用户区资源立即释放
- 内核区数据暂存等待父进程回收
- 最终由操作系统删除进程控制块
- 进入条件:
-
扩展转换路径:
- 新建态→就绪态:完成资源初始化
- 运行态→终止态:正常/异常结束
- 阻塞态→终止态:强制终止等待中的进程
- 就绪态→终止态:直接终止未运行的进程
- 设计意义:
- 更完整描述进程生命周期
- 明确资源管理边界(新建态无资源,终止态释放资源)
- 支持异常处理机制(强制终止路径)
3. 进程相关指令
查看进程

- 基本命令:ps aux 或 ps ajx,其中ps表示process(进程)
- 参数说明:
- a:显示终端上所有进程(包括其他用户的进程)
- u:显示进程详细信息
- x:显示没有控制终端的进程
- j:列出与作业控制相关的信息
- 输出信息
ps aux


USER:进程所属用户
- PID:进程ID(唯一编号)
- %CPU:CPU使用率
- %MEM:内存使用率
- TTY:进程所属终端
- STAT:进程状态(D不可中断/R运行中/S休眠/T停止/Z僵尸等)
- START:进程启动时间
- TIME:进程运行时间
- COMMAND:执行令
ps ajx
深入了解 Linux 查看进程的指令:ps ajx 及其扩展用法-CSDN博客


- 特殊状态说明:

- 父子进程关系:
- PPID:父进程ID
- PID:子进程ID
- PGID:进程组ID(类似教室概念)
- SID:会话ID(类似学校概念)
实时显示进程动态

- 基本命令:top [-d 刷新间隔]
- 交互命令:
- M:按内存使用量排序
- P:按CPU占有率排序
- T:按进程运行时间排序
- U:按用户名筛选进程
- K:杀死指定PID的进程
- q:退出top
- 显示信息:
- 系统负载(load average)
- 任务总数及状态分布
- CPU使用情况
- 内存和交换空间使用情况
- 各进程详细信息
杀死进程(kill名并不是去杀死一个进程,而是给进程发送某个信
号)

基本命令:
- kill [-信号] PID
- killall 进程名
常用信号:
- 9)SIGKILL:强制终止进程
- 15)SIGTERM:正常终止进程(默认)
- 查看所有信号:kill -l

- 后台运行:在命令后加&符号(如./a.out &)

注意事项:
- 杀死终端进程会导致连接断开
- 前台运行会阻塞当前终端
- 后台运行仍会输出到当前终端
实例:

(pid 记得换成真正的进程号)

4. 进程号相关函数
1.进程号的定义与范围
- 类型标识:每个进程由进程号(PID)标识,类型为
- 取值范围:进程号范围是0~32767,操作系统中的进程数量是固定的
- 唯一性保证:进程号总是唯一的,但可以重用(终止后的进程号可被新进程使用)
2. 父进程与父进程号(PPID)
- 创建关系:除init进程(操作系统内核初始进程)外,所有进程都由另一个进程创建
- 父子关系:创建者称为父进程,其进程号为PPID(父进程号)
- 终端示例:运行程序时,其父进程通常是当前终端进程(如在终端执行./a.out,终端的进程就是其父进程)
3. 进程组与进程组号(PGID)
- 集合特性:进程组是一个或多个相关联进程的集合
- 信号处理:同一进程组可接收来自终端的相同信号
- 默认规则:默认情况下,当前进程号(PID)会被当作进程组号(PGID)
4.进程号和进程组相关函数:
5. 进程创建
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。


1. fork函数
函数定义与基本概念
- 创建机制: fork()通过复制调用进程(父进程)来创建新进程(子进程)
- 进程关系:
- 新创建的进程称为子进程(child process)
- 调用进程称为父进程(parent process)
- 内存特性:
- 父子进程运行在独立的内存空间
- fork()时两个内存空间内容相同
- 后续内存写入、文件映射(mmap)和解映射(munmap)互不影响
函数原理***(在1:46处)

父子进程区别点
- PID差异:
- 子进程拥有唯一的进程ID
- 不与任何现有进程组或会话ID匹配
- 继承关系:
- 子进程的父进程ID与父进程的进程ID相同
- 资源限制:
- 不继承父进程的内存锁(mlock/mlockall)
- 进程资源利用率(getrusage)和CPU时间计数器(times)被重置
fork函数的返回值
- 成功返回值:
- 父进程:返回创建的子进程ID(PID)
- 子进程:返回0
- 失败返回值:
- 父进程:返回-1,且不创建子进程
- errno设置:系统会自动设置错误码
- 区分方法:通过检查fork()的返回值来区分当前是父进程还是子进程
fork函数的失败原因
- 系统限制:
- 进程数超限:当系统进程数达到上限时,errno被设置为EAGAIN
- 内存不足:系统内存不足时,errno被设置为ENOMEM
- 错误处理:
- 父进程收到-1返回值时应检查errno值
- 常见errno值包括EAGAIN和ENOMEM两种主要情况
实例:
父进程与子进程的执行流程
父进程执行路径:
- 从main函数开始执行
- 执行fork()调用
- 执行PID>0的条件分支
- 执行后续未加判断的for循环
- 不会执行else if(PID==0)分支
子进程执行路径:
- 从fork()调用后的判断开始执行
- 不会执行fork()调用前的代码
- 执行PID==0的条件分支
- 执行后续未加判断的for循环
- 不会执行PID>0的分支
变量继承机制:
子进程会完全复制父进程的内存空间,包括栈空间的变量。例如父进程中定义,子进程初始也会得到相同的值。
独立性验证:
父进程对执行操作后变为20
子进程对执行操作后变为110
实验证明两个进程的变量修改互不影响
内存空间特性:
父子进程虽然初始变量值相同,但各自拥有独立的内存空间,修改操作只在各自空间生效。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int num = 10;
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
// printf("pid : %d\n", pid);
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
printf("parent num : %d\n", num);
num += 10;
printf("parent num += 10 : %d\n", num);
} else if(pid == 0) {
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
printf("child num : %d\n", num);
num += 100;
printf("child num += 100 : %d\n", num);
}
// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
sleep(1);
}
return 0;
}
/*
实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。
内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。
只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。
注意:fork之后父子进程共享文件,
fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
*/
父子进程虚拟地址空间

用户区克隆:
- 子进程完全复制父进程的用户区数据
- 包括代码段、数据段、堆栈等
内核区差异:
- 大部分内核区数据被复制
- 进程ID(PID)不同:
- 父进程保留原PID
- 子进程获得新PID
注意:
实例中的代码所创建的pid是局部变量,存在虚拟空间的栈空间内,父进程的pid>0,子进程的pid=0;要与内核区的pid作区分。
代码共享机制:
- 父子进程拥有相同的代码段
- 通过局部变量PID的值决定执行路径
执行位置:
- 子进程从fork()调用后的位置开始执行
- 不会重复执行fork()前的代码
变量存储:
- PID作为局部变量存储在各自的栈空间
- 父子进程的栈空间相互独立
2.GDB多进程调试
(1:58)

设置调试进程

follow-fork-mode:
- parent(默认):只调试父进程,子进程继续执行直到结束
- child:只调试子进程,父进程继续执行直到结束
- 查看命令:show follow-fork-mode
- 设置命令:set follow-fork-mode child/parent
detach-on-fork:
- on(默认):非调试进程脱离gdb控制独立运行(非调试进程如子进程不会停在断点处,会全部执行)
- off:非调试进程挂起在fork点,等待后续切换调试
- 典型组合:
- set follow-fork-mode parent + detach-on-fork on(默认配置)
- set follow-fork-mode child + detach-on-fork off(调试子进程时挂起父进程)
info inferiors
用info feriors就可以看到当前进程有几个

- inferior概念:GDB为每个进程创建inferior信息,可通过info inferiors查看所有进程状态。
- 进程切换:使用inferior <编号>命令切换调试的进程,编号对应info inferiors显示的顺序(1为父进程,2为子进程)。
- 调试流程:
- 父进程调试时子进程会在fork处挂起
- 切换inferior后需先用continue让子进程执行到断点
- 使用next进行单步调试
6. exec 函数族介绍
Linux进程——exec族函数、exec族函数与fork函数的配合-CSDN博客
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下运行。


1)基本作用
- 核心功能:根据指定文件名或路径执行可执行程序
- 执行机制:
- 替换原理:用新程序内容完全替换调用进程的用户区数据(代码段、数据段、堆栈等)
- 保留信息:仅保持原进程ID等内核区信息不变,实现"金蝉脱壳"效果
- 执行起点:从新程序的main函数开始执行
2)典型使用场景
- 标准流程:
- 先调用fork()创建子进程
- 在子进程中调用exec函数
- 替换子进程内容执行新程序
- 错误处理:
- 成功:无返回值(原进程内容已被替换)
- 失败:返回-1,程序继续执行后续代码
3)函数分类
- 标准库函数:
- execle()
- execlp()
- execv()
- execvp()
- execvpe()
- 系统调用:
- execve()(Linux/Unix系统原生函数)
- 常用函数:
-
execl()
-

- execlp()(最常用的两个变体)

- 参数传递方式:
- 数组形式(execv):参数通过字符串数组传递
- 列表形式(execl):参数逐个传递,以NULL结尾
4)内存替换原理
- 替换范围:
- 用户区:代码段、数据段、堆、栈等全部被新程序覆盖
- 内核区:PCB(进程控制块)信息保持不变
- 执行效果:
- 原进程"外壳"保留(进程ID等)
- 实际执行的是新程序的代码逻辑
- 类似"借壳运行"的执行模式
7. 进程控制
进程退出


- 函数区别:
- exit()是标准C库函数,位于stdlib.h头文件
- _exit()是Linux系统函数,位于unistd.h头文件
- 参数说明:
- 两者都接收int类型参数表示退出状态
- 父进程可通过此参数获取子进程退出原因
- 功能差异:
- exit()会额外执行:调用退出处理函数、刷新IO缓冲、关闭文件描述符
- _exit()直接终止进程,不做缓冲处理
- 调用关系:
- exit()底层最终会调用_exit()系统调用
- return 0在main函数中等效于exit(0)
实例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
printf("hello\n");
printf("world");
// exit(0);
_exit(0);
return 0;
}
实验现象:
- 使用_exit()时,仅输出"hello\n","word"丢失
- 使用exit()时,"hello\n"和"word"都能正常输出
原因分析:
- printf带\n会自动刷新缓冲区
- 无\n的输出会暂存缓冲区
- _exit()不刷新缓冲区导致数据丢失
编程建议:
- 常规开发优先使用exit()
- 需要立即终止时考虑_exit()
孤儿进程
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan
Process)。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
定义条件:
- 父进程已终止
- 子进程仍在运行
系统处理:
- 内核将孤儿进程的父进程设为init(PID=1)
- init进程负责回收孤儿进程资源
资源回收:
- 用户区数据:子进程可自行释放
- 内核区PCB:必须由父进程(或init)回收
特性说明:
- 孤儿进程本身无害
- 终端显示异常是因进程前后台切换导致
僵尸进程
每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程去释放。
进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
僵尸进程概念
- 定义:僵尸进程是指子进程终止后,父进程尚未回收其内核区PCB资源的状态进程
- 核心特征:
- 进程用户区数据(栈、堆等)可自行释放
- 内核区PCB必须由父进程通过wait/waitpid系统调用释放
- 英文术语为"defunct"(不存在的)

- 识别标志:在ps命令中显示状态为"Z"
- 不可终止性:无法通过kill -9命令杀死,因其已非正常进程
产生机制
必要条件:
- 子进程已终止运行
- 父进程未调用wait/waitpid回收资源
- 父进程持续运行未退出
资源残留:
- 主要保留PCB中的进程ID、父进程ID等内核数据
进程号占用:
- 僵尸进程会持续占用系统进程号资源
系统危害
- 进程号耗尽:系统可分配的进程号数量有限
- 连锁反应:大量僵尸进程会导致系统无法创建新进程
- 资源浪费:内核数据结构无法被重复利用
进程回收
- 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
- 父进程可以通过调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
- wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
- 注意:一次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。
pid_t wait(int *status)

实例:
int main() {
// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
// 创建5个子进程
for(int i = 0; i < 5; i++) {
pid = fork();
if(pid == 0) {
break;
}
}
if(pid > 0) {
// 父进程
while(1) {
printf("parent, pid = %d\n", getpid());
// int ret = wait(NULL);
int st;
int ret = wait(&st);
if(ret == -1) {
break;
}
if(WIFEXITED(st)) {
// 是不是正常退出
printf("退出的状态码:%d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
// 是不是异常终止
printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
sleep(1);
}
} else if (pid == 0){
// 子进程
while(1) {
printf("child, pid = %d\n",getpid());
sleep(1);
}
exit(0);
}
return 0; // exit(0)
}
waitpid()函数
#include <sys/wait.h>
#include <sys/types.h>
pid_t waitpid(pid_t pid, int *status, int options)
进程同步:wait()和waitpid()函数_waitpid函数-CSDN博客
进程等待----wait函数/waitpid函数-CSDN博客
8. 进程间通信
进程间通讯概念
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
- 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
Linux 进程间通信的方式

匿名管道(管道)
管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。
统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两个进程来分别执行 ls 和wc。
管道的特点

- 管道其实是一个在内核内存中维护的缓冲器,也是一个pipe文件,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
- 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
- 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
- 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
- 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
- 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用lseek()来随机的访问数据。
- 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。

匿名管道的使用
创建匿名管道(pipe)


实例:
// 子进程发送数据给父进程,父进程读取到数据输出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());
// 关闭写端,其实就是关闭相父进程写数据的文件
close(pipefd[1]);
// 从管道的读取端读取数据
char buf[1024] = {0};
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("parent recv : %s, pid : %d\n", buf, getpid());
// 向管道中写入数据
//char * str = "hello,i am parent";
//write(pipefd[1], str, strlen(str));
//sleep(1);
}
} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
char * str = "hello,i am child";
write(pipefd[1], str, strlen(str));
//sleep(1);
// int len = read(pipefd[0], buf, sizeof(buf));
// printf("child recv : %s, pid : %d\n", buf, getpid());
// bzero(buf, 1024);
}
}
return 0;
}
查看管道缓冲大小命令

查看管道缓冲大小函数

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int pipefd[2];
int ret = pipe(pipefd);
// 获取管道的大小
long size = fpathconf(pipefd[0], _PC_PIPE_BUF);
printf("pipe size : %ld\n", size);
return 0;
}
管道的读写特点:
使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作)
1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端
读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。
2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程
也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后,
再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。
3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程
向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。
4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程
也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞,
直到管道中有空位置才能再次写入数据并返回。
总结:
读管道:
管道中有数据,read返回实际读到的字节数。
管道中无数据:
写端被全部关闭,read返回0(相当于读到文件的末尾)
写端没有完全关闭,read阻塞等待
写管道:
管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)
管道读端没有全部关闭:
管道已满,write阻塞
管道没有满,write将数据写入,并返回实际写入的字节数
实例:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置新的flag
*/
int main() {
// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1) {
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0) {
// 父进程
printf("i am parent process, pid : %d\n", getpid());
// 关闭写端
close(pipefd[1]);
// 从管道的读取端读取数据
char buf[1024] = {0};
int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag
while(1) {
int len = read(pipefd[0], buf, sizeof(buf));
printf("len : %d\n", len);
printf("parent recv : %s, pid : %d\n", buf, getpid());
memset(buf, 0, 1024);
sleep(1);
}
} else if(pid == 0){
// 子进程
printf("i am child process, pid : %d\n", getpid());
// 关闭读端
close(pipefd[0]);
char buf[1024] = {0};
while(1) {
// 向管道中写入数据
char * str = "hello,i am child";
write(pipefd[1], str, strlen(str));
sleep(5);
}
}
return 0;
}
有名管道(命名管道)
1. 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。
2. 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
3. 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。
4. 有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于:FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。
创建
通过命令创建有名管道

通过函数创建有名管道


实例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 判断文件是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
printf("管道不存在,创建管道\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
return 0;
}
注意事项:
有名管道的注意事项:
1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道
2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道
读管道:
管道中有数据,read返回实际读到的字节数
管道中无数据:
管道写端被全部关闭,read返回0,(相当于读到文件末尾)
写端没有全部被关闭,read阻塞等待
写管道:
管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)
管道读端没有全部关闭:
管道已经满了,write会阻塞
管道没有满,write将数据写入,并返回实际写入的字节数。
阻塞特性(易理解版):
- 读端阻塞: 当没有写端打开管道时,读端会阻塞,直到有写端打开管道并开始写入数据。
- 写端阻塞: 当管道已满且没有读端读取数据时,写端会阻塞,直到有读端开始读取数据或管道中有空余空间。
信号处理:
- 写端异常终止: 当所有读端关闭时,如果写端继续往管道中写数据,会导致写端收到SIGPIPE信号并异常终止。
读写行为:
- 读行为:
- 管道中有数据时,read函数返回实际读到的字节数。
- 管道中无数据且所有写端关闭时,read函数返回0,表示读到文件末尾。
- 管道中无数据但写端未全部关闭时,read函数会阻塞,等待数据写入。
- 写行为:
- 管道未满时,write函数将数据写入管道并返回实际写入的字节数。
- 管道已满时,write函数会阻塞,直到管道中有空余空间。
实例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// 向管道中写数据
int main() {
// 1.判断文件是否存在
int ret = access("test", F_OK);
if(ret == -1) {
printf("管道不存在,创建管道\n");
// 2.创建管道文件
ret = mkfifo("test", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
// 3.以只写的方式打开管道
int fd = open("test", O_WRONLY);
if(fd == -1) {
perror("open");
exit(0);
}
// 写数据
for(int i = 0; i < 100; i++) {
char buf[1024];
sprintf(buf, "hello, %d\n", i);
printf("write data : %s\n", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
// 从管道中读取数据
int main() {
// 1.打开管道文件
int fd = open("test", O_RDONLY);
if(fd == -1) {
perror("open");
exit(0);
}
// 读数据
while(1) {
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
if(len == 0) {
printf("写端断开连接了...\n");
break;
}
printf("recv buf : %s\n", buf);
}
close(fd);
return 0;
}
有名管道与匿名管道的基本区别
- 有名管道: 有文件实体,操作的是内核中的缓冲区,可以在无关系的进程间通信。
- 匿名管道: 无文件实体,操作的是内核中的缓冲区,只能在有关系的进程(如父子进程)间通信。
使用有名管道完成聊天功能

chatA
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 1.判断有名管道文件是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
ret = access("fifo2", F_OK);
if(ret == -1) {
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
// 2.以只写的方式打开管道fifo1
int fdw = open("fifo1", O_WRONLY);
if(fdw == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo1成功,等待写入...\n");
// 3.以只读的方式打开管道fifo2
int fdr = open("fifo2", O_RDONLY);
if(fdr == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo2成功,等待读取...\n");
char buf[128];
// 4.循环的写读数据
while(1) {
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1) {
perror("write");
exit(0);
}
// 5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0) {
perror("read");
break;
}
printf("buf: %s\n", buf);
}
// 6.关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
chatB
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
int main() {
// 1.判断有名管道文件是否存在
int ret = access("fifo1", F_OK);
if(ret == -1) {
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo1", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
ret = access("fifo2", F_OK);
if(ret == -1) {
// 文件不存在
printf("管道不存在,创建对应的有名管道\n");
ret = mkfifo("fifo2", 0664);
if(ret == -1) {
perror("mkfifo");
exit(0);
}
}
// 2.以只读的方式打开管道fifo1
int fdr = open("fifo1", O_RDONLY);
if(fdr == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo1成功,等待读取...\n");
// 3.以只写的方式打开管道fifo2
int fdw = open("fifo2", O_WRONLY);
if(fdw == -1) {
perror("open");
exit(0);
}
printf("打开管道fifo2成功,等待写入...\n");
char buf[128];
// 4.循环的读写数据
while(1) {
// 5.读管道数据
memset(buf, 0, 128);
ret = read(fdr, buf, 128);
if(ret <= 0) {
perror("read");
break;
}
printf("buf: %s\n", buf);
memset(buf, 0, 128);
// 获取标准输入的数据
fgets(buf, 128, stdin);
// 写数据
ret = write(fdw, buf, strlen(buf));
if(ret == -1) {
perror("write");
exit(0);
}
}
// 6.关闭文件描述符
close(fdr);
close(fdw);
return 0;
}
9.内存映射
3.2_5_内存映射文件_哔哩哔哩_bilibili
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。

内存映射相关系统调用

mmap函数

munmap函数

使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信
注意:内存映射区通信,是非阻塞。
举例:
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
int main() {
// 1.打开一个文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END); // 获取文件的大小
// 2.创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 3.创建子进程
pid_t pid = fork();
if(pid > 0) {
wait(NULL);
// 父进程
char buf[64];
//将ptr中的内容复制到buf中
strcpy(buf, (char *)ptr);
printf("read data : %s\n", buf);
}else if(pid == 0){
// 子进程
strcpy((char *)ptr, "nihao a, father!!!");
}
// 关闭内存映射区
munmap(ptr, size);
return 0;
}
进程之间通讯的内容会存在所创建的test文件中。
内存映射注意事项
1. 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
可以对ptr进行++操作,但是munmap函数需要调用内存首地址,++操作后会错误,因此需要提前保存++前的内存地址。
2. 如果open时O_RDONLY, mmap时prot参数指定 PROT_READ | PROT_WRITE 会怎样?
错误, 会返回 MAP_FAILED (即,void* -1)
open() 函数中的权限建议和prot参数的权限保持一致。
3. 如果文件偏移量为1000会怎样?
偏移量必须是4k的整数倍, 返回MAP_FAILED (分页大小)
4. mmap什么情况下会调用失败?
- 第二个参数: length = 0
- 第三个参数: prot权限有问题(prot的权限没有<=open的权限)
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
第五个参数fd 通过open函数时 未指定 O_RDONLY | O_WRONLY
5. 可以open的时候O_CREATE一个新文件来创建映射区吗?
- 可以, 但是创建文件大小为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()
6. mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("xxx")
mmap(,,, fd, 0);
close(fd);
映射区还存在, 创建映射区的fd被关闭, 没有任何影响
7. 对ptr越界操作会怎样?
越界操作,操作的是非法内存,-> 段错误
通过内存映射来实现文件复制
基本原理
- 通信机制:内存映射不仅可以用于进程间通信(无论进程是否有关系),还能实现文件复制功能
- 操作优势:通过直接操作内存数据实现文件复制,相比传统文件IO操作速度更快
- 同步机制:内存中的数据修改会自动同步到映射文件中
实现步骤
- 原始文件映射:
- 打开原始文件(如english.txt)
- 使用mmap将文件数据映射到内存空间
- 获取文件大小用于后续操作
- 目标文件准备:
- 创建新文件(如copy.txt)
- 使用ftruncate/lseek将新文件拓展至原始文件大小
- 对新文件执行写操作确保文件有效性
- 内存操作:
- 将新文件也映射到内存空间
- 使用memcpy将原始文件内存数据拷贝到新文件内存区域
- 资源释放:
- 按创建顺序逆序释放资源
- 先munmap后关闭文件描述符
- 遵循"后创建先释放"原则处理依赖关系
关键代码实现
// 1. 打开原始文件
int fd = open("english.txt", O_RDWR);
// 2. 创建新文件
int fd1 = open("copy.txt", O_RDWR|O_CREAT, 0644);
// 3. 获取文件大小并拓展新文件
off_t len = lseek(fd, 0, SEEK_END);
ftruncate(fd1, len);
// 4. 内存映射
void* ptr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
void* ptr1 = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd1, 0);
// 5. 内存拷贝
memcpy(ptr1, ptr, len);
// 6. 资源释放
munmap(ptr1, len);
munmap(ptr, len);
//fd对fd1存在依赖,所以先关闭fd1
close(fd1);
close(fd);
注意事项
- 文件大小限制:不适合处理超大文件(如1GB以上),可能造成内存不足
- 权限设置:需要确保对原始文件有读权限,对新文件有写权限
- 错误处理:每个系统调用都应检查返回值,确保操作成功
- 头文件包含:需要包含sys/mman.h、unistd.h、string.h等必要头文件
性能特点
- 高效性:直接内存操作避免了多次read/write系统调用
- 简化性:将文件视为内存区域处理,编程模型更简单
- 局限性:受限于物理内存大小,不适合处理超大文件
匿名映射
- 匿名映射定义: 匿名映射是一种不需要文件实体进行内存映射的方式。
- 用途: 匿名映射主要用于父子进程间的通信,无法用于无关系进程间的通信,因为匿名映射没有文件实体,不同进程间无法通过文件关联。
- 参数设置:
- 地址: 第一个参数,通常设为NULL,让系统决定映射地址。
- 长度: 第二个参数,指定映射的内存大小,如4096字节(4K)。
- 权限: 第三个参数,如PROT_READ | PROT_WRITE,表示可读可写。
- flags: 关键参数,使用MAP_ANONYMOUS或MAP_ANON表示匿名映射。
- fd: 文件描述符,对于匿名映射应设为-1,因为该参数会被忽略。
- 偏移量: 应设为0,因为没有文件实体,偏移量无意义。
- 初始化: 匿名映射的内容被初始化为零。
- 注意事项:
- 示例代码主要用于理解匿名映射和父子进程通信,实际开发中需根据具体业务场景调整。
- 匿名映射是非阻塞的,若子进程在父进程写入前读取,将无法读到数据。
/*
匿名映射:不需要文件实体进程一个内存映射
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
// 1.创建匿名内存映射区
int len = 4096;
//无文件实体,故fd=-1;
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 父子进程间通信
pid_t pid = fork();
if(pid > 0) {
// 父进程
strcpy((char *) ptr, "hello, world");
wait(NULL);
}else if(pid == 0) {
// 子进程
sleep(1);
printf("%s\n", (char *)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if(ret == -1) {
perror("munmap");
exit(0);
}
return 0;
}
10.信号
概述
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号(九号信号)。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。
- 比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
- 运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是:
- 让进程知道已经发生了一个特定的事情。
- 强迫进程执行它自己代码中的信号处理程序。
信号的特点:
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送
- 优先级比较高
信号表
查看系统定义的信号列表:kill –l
前 31 个信号为常规信号,其余为实时信号。

共有62个信号,少了32,33两个信号




信号的 5 种默认处理动作

信号相关的函数

kill

raise

abort

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if(pid == 0) {
// 子进程
int i = 0;
for(i = 0; i < 5; i++) {
printf("child process\n");
sleep(1);
}
} else if(pid > 0) {
// 父进程
printf("parent process\n");
sleep(2);
printf("kill child process now\n");
kill(pid, SIGINT);
}
return 0;
alarm

// 1秒钟电脑能数多少个数?
#include <stdio.h>
#include <unistd.h>
/*
实际的时间 = 内核时间 + 用户时间 + 消耗的时间
进行文件IO操作的时候比较浪费时间
定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
*/
int main() {
alarm(1);
int i = 0;
while(1) {
printf("%i\n", i++);
}
return 0;
}
setitimer

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct itimerval new_value;
// 设置间隔的时间,之后每两秒发送一次信号
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时,即三秒后发送信号
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
信号捕捉函数

signal

signum参数:
- 指定要捕捉的信号编号
- 建议使用宏定义而非直接数字(如SIGALRM而非14)
handler参数:
- SIG_IGN: 忽略该信号
- SIG_DFL: 使用默认处理方式
- 回调函数: 自定义信号处理逻辑
回调函数特点:
- 由内核调用而非程序员直接调用
- 需提前定义好函数实现
- 函数指针类型为void (*)(int)
- 参数int表示捕捉到的信号编号
回调函数举例
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
// 注册信号捕捉
// signal(SIGALRM, SIG_IGN);
// signal(SIGALRM, SIG_DFL);
// void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。
signal(SIGALRM, myalarm);
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
sigaction函数

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void myalarm(int num) {
printf("捕捉到了信号的编号是:%d\n", num);
printf("xxxxxxx\n");
}
// 过3秒以后,每隔2秒钟定时一次
int main() {
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;//直接写函数名
sigemptyset(&act.sa_mask); // 清空临时阻塞信号集
// 注册信号捕捉
sigaction(SIGALRM, &act, NULL);
struct itimerval new_value;
// 设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
// 设置延迟的时间,3秒之后开始第一次定时
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的
printf("定时器开始了...\n");
if(ret == -1) {
perror("setitimer");
exit(0);
}
// getchar();
while(1);
return 0;
}
内核实现信号捕捉的过程

在执行主控制流程的某条指令时,因为中断异常或者,系统调用呢,进入内核啊,比如说我们运行这个main函数,我们执行了一个系统调用,或者是出现了中断,出现了异常,这时回进入到内核,内核把这个异常处理完毕,然后准备回到这个用户模式之前。他要先干嘛呢?他要先处理当前进程中可以递送的信号,对吧?他要去处理你这些啊,要递送的信号,那么他会执行一个。这个due signal,这是内核系统啊,内核底层的这个函数啊due signal。去干嘛呢?
去发送这个信号啊。那么,如果信号的处理动作为自定义的信号处理函数,那么注意了这个自定义的信号处理函数是我们程序员定义的吧?它是。执行的话,应该是在用户区执行,对不对?那么这个时候内核呢?它会干嘛呢?则回到用户模式去执行信号处理函数。你看是不是回到用户模式去执行这个,我们自定义好的这个信号处理函数啊是吧,而不是回到主控制流程,而不是回到这个地方。
看不是回到这个,我们刚才中断的这个地方,继续往下执行,而是去处理这个什么呀,回调函数是吧,这个时候大家应该能理解吧,这个回调函数是内核去。帮我们去执行的,当他接收到这个信号以后,就说他当这个接收到这个信号以后啊,他去处理的时候就用我们自己写的这个。啊,回调函数去处理啊,那么信号呢?处理函数返回时呢?
执行特殊的系统调用叫做sig return。啊,这个又是一个特殊的一个系统调用啊,再次返回内核,返回到这个system secret return啊。那么到了内核呢?返回用户模式,从主控制流程中,上次中断的地方继续向下运行啊。什么意思呢?就是说你在这儿中断了,你是不是应该要保留啊?我这个当前中断的一些信息啊,是吧?那么当我这个回调函数就是我这个信号处理,
处理完了以后,我这个内核是不是又要回到?这个用户模式回到你当时中断的地方,从中断的地方继续往下执行啊,对吧?哎,它就是这样的一个流程。
信号捕捉的特性
1.阻塞机制
临时信号集切换:
- 执行回调函数时使用临时阻塞信号集
- 回调结束后恢复内核原始阻塞信号集
自动屏蔽特性:
- 处理某信号(如SIGALRM)期间,同类型信号自动被屏蔽
- 前次回调完成后才处理后续同类信号
位图限制:
- 未决信号集采用位图记录(0/1状态)
- 常规信号不支持排队,重复信号仅记录正在处理信号的后一次
2.信号区别
- 实时信号特殊说明:实时信号(后32个)支持排队机制
- 常规信号局限:相同信号多次到达时,除第一次外其余均被丢弃
SIGCHILD信号
1.SIGCHLD信号产生的条件
- 子进程终止时
- 子进程接收到 SIGSTOP 信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号
2.SIGCHLD信号的作用
解决僵尸进程问题
- 核心原理:通过捕捉SIGCHLD信号实现异步回收,避免父进程阻塞等待
- 传统方法缺陷:
- 使用wait/waitpid需要父进程主动轮询
- wait函数会阻塞父进程执行
- waitpid虽然可以非阻塞但编码复杂
- 信号处理优势:
- 父进程可继续执行主要业务逻辑
- 只在收到信号时才进行回收操作
- 实现真正的异步处理机制
子进程回收机制
- wait函数问题:
- 未决信号集只能记录一个信号状态
- 多个子进程同时结束时,后续信号会被丢弃
- 导致部分僵尸进程无法被回收
- 解决方案:
- 使用while循环配合waitpid()非阻塞回收
- waitpid()参数设置:
- -1表示回收任意子进程
- WNOHANG表示非阻塞模式
- 返回值处理:
- ==0:还有子进程存活
- ==-1:所有子进程已回收
信号注册的竞态问题
- 问题现象:
- 信号捕捉未完成注册时子进程已结束
- 导致部分SIGCHLD信号丢失
- 出现段错误(core dumped)
- 解决方案:
- 先阻塞SIGCHLD信号
- 完成信号捕捉注册后再解除阻塞
- 确保信号处理程序就绪后才接收信号
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
void myFun(int num) {
printf("捕捉到的信号 :%d\n", num);
// 回收子进程PCB的资源
// while(1) {
// wait(NULL);
// }
while(1) {
int ret = waitpid(-1, NULL, WNOHANG);
if(ret > 0) {
printf("child die , pid = %d\n", ret);
} else if(ret == 0) {
// 说明还有子进程或者
break;
} else if(ret == -1) {
// 没有子进程
break;
}
}
}
int main() {
// 提前设置好阻塞信号集,阻塞SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigprocmask(SIG_BLOCK, &set, NULL);
// 创建一些子进程
pid_t pid;
for(int i = 0; i < 20; i++) {
pid = fork();
if(pid == 0) {
break;
}
}
if(pid > 0) {
// 父进程
// 捕捉子进程死亡时发送的SIGCHLD信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myFun;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
// 注册完信号捕捉以后,解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
while(1) {
printf("parent process pid : %d\n", getpid());
sleep(2);
}
} else if( pid == 0) {
// 子进程
printf("child process pid : %d\n", getpid());
}
return 0;
}
不理解
信号集
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制(二进制数01表示)来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。

进程的处理过程
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
- 阻塞信号集默认不阻塞任何的信号
- 如果想要阻塞某些信号需要用户调用系统的API
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
- 如果没有阻塞,这个信号就被处理
- 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
信号集相关操作函数


sigemptyset函数
- 功能: 清空信号集中的所有数据,将所有标志位置零
- 参数:
- sigset_t *set: 需要操作的信号集指针(传出参数)
- sigset_t是一个64位的二进制数。
- 返回值:
- 成功返回0,失败返回-1
- 注意事项:
- 新创建的信号集变量初始值是随机的,必须先用此函数清空
- 清空操作是为了避免随机值可能引发的问题
sigfillset函数
- 功能: 填充信号集,将所有标志位置一
- 参数:
- sigset_t *set: 需要操作的信号集指针
- 返回值:
- 成功返回0,失败返回-1
sigaddset函数
- 功能: 向信号集中添加信号,设置指定信号对应的标志位为一
- 参数:
- sigset_t *set: 需要操作的信号集指针
- int signum: 需要设置阻塞的信号编号
- 返回值:
- 成功返回0,失败返回-1
sigdelset函数
- 功能: 从信号集中删除信号,设置指定信号对应的标志位为零
- 参数:
- sigset_t *set: 需要操作的信号集指针
- int signum: 需要设置不阻塞的信号编号
- 返回值:
- 成功返回0,失败返回-1
sigismember函数
- 功能: 判断信号是否在信号集中(是否被阻塞)
- 参数:
- const sigset_t *set: 需要检查的信号集指针(const表示不会被修改)
- int signum: 需要判断的信号编号
- 返回值:
- 1表示信号被阻塞
- 0表示信号不阻塞
- -1表示调用失败
#include <signal.h>
#include <stdio.h>
int main() {
// 创建一个信号集
sigset_t set;
// 清空信号集的内容
sigemptyset(&set);
// 判断 SIGINT 是否在信号集 set 里
int ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 添加几个信号到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 判断SIGINT是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0) {
printf("SIGINT 不阻塞\n");
} else if(ret == 1) {
printf("SIGINT 阻塞\n");
}
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);
// 判断SIGQUIT是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0) {
printf("SIGQUIT 不阻塞\n");
} else if(ret == 1) {
printf("SIGQUIT 阻塞\n");
}
return 0;
}
sigprocmask系统调用


- 核心功能:将自定义信号集数据设置到内核中,提供三种操作方式
- 参数how:
- SIG_BLOCK:添加阻塞(按位或操作)
- SIG_UNBLOCK:解除阻塞(先取反再按位与)
- SIG_SETMASK:直接覆盖原值
- 参数set:用户自定义的已初始化信号集
- 参数oldset:保存设置前的内核阻塞信号集状态(可设为NULL)
操作流程:
- 用户自定义信号集(如设置2/6/9号信号为1)
- 通过sigprocmask将自定义集设置到内核
- 内核根据how参数决定操作方式
位运算示例:

sigpending系统调用

- 功能: 用于获取内核中的未决信号集信息
- 参数:
- 传入一个sigset_t类型的传出参数set
- 系统会将内核中的未决信号集数据填充到该参数中
- 返回值:
- 成功返回0
- 失败返回-1并设置errno
- 使用场景: 通常与sigprocmask配合使用,用于检查被阻塞的信号是否已到达
实例:
// 编写一个程序,把所有的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);
// 将2号和3号信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while(1) {
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
// 遍历前32位
for(int i = 1; i <= 31; i++) {
if(sigismember(&pendingset, i) == 1) {
printf("1");
}else if(sigismember(&pendingset, i) == 0) {
printf("0");
}else {
perror("sigismember");
exit(0);
}
}
printf("\n");
sleep(1);
if(num == 10) {
// 解除阻塞
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}
11.共享内存
- 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制(进程间通信机制)无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
- 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存(管道文件其实就是内核中的文件)和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快(如下图)

使用步骤
- 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
- 使用 shmat() 来附上共享内存段(其实就是将进程与该内存段连接,然后才能使用),即使该段成为调用进程的虚拟内存的一部分。
- 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat()调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
- 调用 shmdt() 来分离共享内存段(其实就是断开连接)。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
- 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
相关函数

头文件:
#include <sys/ipc.h>
#include <sys/shm.h>
shmget

shmat

shmdt

shmctl

ftok

实例:
//写入
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {
// 1.创建一个共享内存
int shmid = shmget(100, 4096, IPC_CREAT|0664);
printf("shmid : %d\n", shmid);
// 2.和当前进程进行关联
void * ptr = shmat(shmid, NULL, 0);
char * str = "helloworld";
// 3.写数据
memcpy(ptr, str, strlen(str) + 1);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
//读出
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {
// 1.获取一个共享内存
int shmid = shmget(100, 0, IPC_CREAT);
printf("shmid : %d\n", shmid);
// 2.和当前进程进行关联
void * ptr = shmat(shmid, NULL, 0);
// 3.读数据
printf("%s\n", (char *)ptr);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
共享内存操作命令

问题1:操作系统如何知道一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
- shm_nattach 记录了关联的进程个数
如下图所示:

问题2:可不可以对共享内存进行多次删除 shmctl
- 可以的
- 因为shmctl 标记删除共享内存,不是直接删除(就是共享内存的键全变成0)
- 什么时候真正删除呢?
当和共享内存关联的进程数(连接数)为0的时候,就真正被删除
- 当共享内存的key为0的时候,表示共享内存被标记删除了
如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。
共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效率更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
- 进程突然退出
共享内存还存在
内存映射区消失
- 运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。
12.终端
终端

Linux终端与文件目录详解-CSDN博客和Linux终端详解_哔哩哔哩_bilibili一起看
进程组

会话

三者关系
【Linux 系统】会话和守护进程_linux mint 会话管理-CSDN博客这个讲的很好

函数

守护进程

/*
写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。
*/
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>
void work(int num) {
// 捕捉到信号之后,获取系统时间,写入磁盘文件
time_t tm = time(NULL);
struct tm * loc = localtime(&tm);
// char buf[1024];
// sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
// ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);
// printf("%s\n", buf);
char * str = asctime(loc);
int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
write(fd ,str, strlen(str));
close(fd);
}
int main() {
// 1.创建子进程,退出父进程
pid_t pid = fork();
if(pid > 0) {
exit(0);
}
// 2.将子进程重新创建一个会话
setsid();
// 3.设置掩码
umask(022);
// 4.更改工作目录
chdir("/home/nowcoder/");
// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 6.业务逻辑
// 捕捉定时信号
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = work;
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
struct itimerval val;
val.it_value.tv_sec = 2;
val.it_value.tv_usec = 0;
val.it_interval.tv_sec = 2;
val.it_interval.tv_usec = 0;
// 创建定时器
setitimer(ITIMER_REAL, &val, NULL);
// 不让进程结束
while(1) {
sleep(10);
}
return 0;
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)