回收子进程
孤儿进程
父进程先于子进程结束,子进程的父进程成为 init 进程。
僵尸进程
子进程终止,父进程尚未回收,子进程残留的资源 (PCB) 存放于内核中,变成僵尸 (Zoombie) 进程。
wait 函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪一个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息, 然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 shell 中用特殊变量 $? 查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程
- 父进程调用 wait 函数可以回收子进程终止信息。该函数有三个功能:
-
阻塞等待子进程退出
-
回收子进程残留资源
-
获取子进程结束状态(推出原因)
-
pid_t wait(int *status); 成功:清除掉的子进程ID; 失败:-1(没有子进程)
-
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2.释放用户空间分配的内存。内存的 PCB 仍然存在。其中保存该进程的退出状态(正常终止->退出值;异常终止->终止信号)
**可使用 wait 函数传出的参数 status 来保存进程的退出状态。**借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
-
WIFEXITED(status) 为非 0 -> 进程正常结束
- WEXITSTATUS(status) 如上宏为真,使用此宏 -> 获取进程退出状态( exit 的参数)
-
WIFSIGNALED(status) 为非 0 -> 进程异常终止
- WTERMSIG(status) 如上宏为真,使用此宏 -> 取得使进程终止的哪个信号的编号
-
WIFSTOPPED(status) 为非 0 -> 进程处于暂停状态
- WSTOPSIG(status) 如上宏为真,使用此宏 -> 取得使进程暂停得那个信号得编号。
- WIFCONTINUED(status) 为真 -> 进程暂停后已经继续运行
waitpid 函数
作用同 wait, 但可指定 pid 进程清理,可以不阻塞。
-
pid_t waitpid(pid_t pid, int *status, int options);
- 成功:返回清理掉得子进程 ID;
- 失败:-1(无子进程)
-
特殊参数和返回情况:
-
参数 pid:
-
pid>0: 回收指定 ID 得子进程
-
pid==0: 回收和当前调用 waitpid 一个组任意子进程
-
pid==-1: 回收任意子进程 (相当于 wait)
-
pid<-1: 回收指定进程组内的任意子进程
-
-
参数 options:
- 为 WNOHANG,不阻塞,返回 0,代表子进程正在进行。
- 为 0,阻塞。
注意:一次 wait 或 waitpid 调 用只能清理一个子进程,清理多个子进程应使用循环。
练习:父进程 fork 3 个子进程,三个子进程一个调用 ps 命令,一个调用自定义程序 1(正常),一个调用自定义程序 2(会段错误)。父进程使用 waitpid 对其子进程进行回收。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main(void) {
int i;
for(i = 1; i <= 3; i++) {
pid_t pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
}
if(pid == 0) {
if(i == 1) {
execlp("ps", "ps", "aux", NULL);
} else if(i == 2) {
execl("./normal", "normal", NULL);//当前目录下写个正常程序
} else {
execl("./abnormal", "abnormal", NULL);//当前目录下写个段错误程序
}
break;
}
}
if(i > 3) {
int status;
while(1) {
int t = waitpid(-1, &status, WNOHANG);
if(t == -1) break;
if(t == 0) {
sleep(1);
continue;
}
if(WIFEXITED(status)) {
printf("normal %d %d\n", t, WEXITSTATUS(status));
}
else if(WIFSIGNALED(status)) {
printf("abnormal %d %d\n", t, WTERMSIG(status));
}
}
}
return 0;
}
IPC方法(interProcess Communication)
ulimit 是一个计算机命令,用于shell启动进程所占用的资源,参数形式有-H设置硬资源限制;-S 设置软资源限制;-a 显示当前所有的资源限制等。
Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:**文件、管道、信号、共享内存、消息队列、套接字、命名管道等。**随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方法有:
-
管道(使用最简单)
- pipe
- fifo: (有名管道)
- 用于非血缘关系进程间通信
-
信号(开销最小)
-
共享映射区(无血缘关系)
-
本地套接字(最稳定)
管道的概念:
管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道。有如下特质:
-
其本质是一个伪文件(实为内核缓冲区) // 伪文件不占用磁盘存储 s, b, c, p
-
由两个文件描述符引用,一个表示读端,一个表示写端。
-
规定数据从管道的写端流入管道,从读端流出。
管道的原理:管道实为内核使用环形队列机制,借助**内核缓冲区(4k)**实现。
管道的局限性:
-
数据自己读不能自己写。
-
数据一旦被读走,便不再管道中存在,不可反复读取。
-
由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
-
只能在由公共祖先的进程间使用管道。
常用的通信方式有:单工通信、半双工通信、全双工通信。
pipe 函数
创建管道
-
int pipe(int pipefd[2]);
- 成功: 0;
- 失败: -1; 设置 errno
-
函数调用成功返回 r/w 两个文件描述符。无需 open, 但需手动 close。
-
规定:fd[0] -> r; fd[1] -> w。向管道文件读写数据其实是在读写内核缓冲区。
练习:利用 pipe 函数,子进程向父进程发送信息。
#include <cstdio>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
int main(void)
{
int fd[2];
int ret = pipe(fd);
if(ret == -1) {
perror("pipe error");
exit(1);
}
pid_t pid = fork();
if(pid == -1) {
perror("pid error");
exit(1);
} else if(pid == 0) {
close(fd[0]); //close r
write(fd[1], "parent hello\n", strlen("parent hellor\n"));
} else {
sleep(1);
close(fd[1]);//close w
char buff[2048];
int size = read(fd[0], buff, sizeof(buff));
write(STDOUT_FILENO, buff, size);
}
return 0;
}
管道读写行为
- 读管道
- 管道中有数据
- read 返回实际读到的字节数
- 管道中无数据
- 写端全关闭:read 返回 0
- 仍有写端打开:阻塞等待
- 管道中有数据
- 写管道
- 读端全关闭
- 进程异常终止(SIGPIPE 信号)
- 有读端打开
- 管道未满:写数据,返回写入字节数
- 管道已满:阻塞(少见)
- 读端全关闭
管道优劣
-
优点:实现手段简单
-
缺点
- 单向通信
- 只能有血缘关系进程间使用
FIFO
- 命名管道(Linux 基础文件类型)
- mkfifo 函数
- 参数:
- name
- mode: 8进制
- 返回值:
- 成功:0
- 失败:-1,设置 errno
- 参数:
- 无血缘关系进程间通信
- 使用同一 FIFO
- 可多读端,多写端。
共享存储映射
文件进程间通信
使用文件也可以完成 IPC,理论依据是,fork 后,父子进程共享文件描述符。也就共享打开的文件。
练习:编程测试,父子进程共享打开的文件。借助文件进行进程间通信。
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
void sys_error(const char *s)
{
perror(s);
exit(1);
}
int main(void)
{
pid_t pid;
pid = fork();
int fd;
if(pid == -1) {
sys_error("fork error");
}
else if(pid == 0) {
fd = open("file_IPC", O_RDWR|O_CREAT, 0644);
if(fd == -1) {
sys_error("open error");
}
write(fd, "hello father\n", strlen("hello father\n"));
sleep(2);
char buff[2048];
int size = read(fd, buff, sizeof(buff));
write(STDOUT_FILENO, buff, size);
}
else {
sleep(1);
fd = open("file_IPC", O_RDWR);
if(fd == -1) {
sys_error("open error");
}
char buff[2048];
int size = read(fd, buff, sizeof(buff));
write(STDOUT_FILENO, buff, size);
write(fd, "hello son\n", strlen("hello son\n"));
sleep(1);
}
close(fd);
return 0;
}
存储映射 I/O
存储映射 I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可以在不实用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过 mmap 函数来实现。
mmap 函数
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
-
返回:
- 成功:返回创建的映射区的首地址
- 失败:MAP_FAILED 宏
-
addr: 建立映射区的首地址,由 linux 内核指定。使用时,直接传递NULL
-
length: 欲创建映射区的大小
-
prot: 映射器权限 PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
-
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
- MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
- MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
-
fd: 用来建立映射区的文件描述符
-
offset: 映射文件的偏移(4k 的整数倍)
munmap 函数
int munmap(void *addr, size_t length);
- 返回:
- 成功: 0;
- 失败:-1;
- 参数:
- mmap 返回值
- 映射区大小
mmap 注意事项
- 创建映射区的过程中,隐含着一次对映射文件的读操作。
- 当 MAP_SHARED 时,要求:映射区的权限 <= 文件打开权限(出于对映射区的保护)。而 MAP_PRIVATE 则无所谓,因为 mmap 中的权限是对内存的限制。
- 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭
- 特别注意,当映射文件大小为 0 时,不能创建映射区。所以:用于映射的文件必须要有实际大小!! mmap 使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
- munmap 传入的地址一定是 mmap 的返回地址。坚决杜绝指针++操作。
- 如果文件偏移量必须为 4K 的整数倍(mmu的最小操作单位)
- mmap 创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
mmap 父子进程通信
父子等有血缘关系的进程之间也可以通过 mmap 建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数 flags:
-
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区
-
MAP_SHARED: (共享映射) 父子进程共享映射区
练习:父进程创建映射区,然后 fork 子进程,子进程修改映射区内容,而后,父进程读取映射区内容,查验是否共享。
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
using namespace std;
int var = 100;
int main(void)
{
int fd = open("./fmmap_ps", O_CREAT|O_RDWR|O_TRUNC, 0644);
if(fd < 0) {
perror("open error");
exit(1);
}
unlink("./fmmap_ps");//删除临时文件目录项,当所有占用该文件的进程都结束后,文件就被删除。
ftruncate(fd, 4);
int *p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED) {
perror("mmap error");
exit(1);
}
close(fd);
pid_t pid = fork();
if(pid == -1) {
perror("fork error");
exit(1);
}
else if(pid == 0) {
*p = 5;
var = 5;
printf("p = %d, var = %d\n", *p, var);
}
else {
sleep(1);
printf("p = %d, var = %d\n", *p, var);
}
munmap(p, 4);
return 0;
}
结论:父子进程共享:1. 打开的文件 2. mmap 建立的映射区(但必须要使用 MAP_SHARED)
匿名映射
通过使用发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要 open 一个 temp 文件,创建好了再 unlink、close 掉,比较麻烦。可以直接使用匿名映射来代替。其实 Linux 系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标识位参数 flags 来指定。
使用 MAP_ANONYMOUS(或 MAP_ANON), 如:
-
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
-
“4” 随意举例,该位置表大小,可依据实际需要填写。
需注意的是,MAP_ANONYMOUS 和 MAP_ANON 这两个宏是 linux 操作系统特有的宏。
在类 Unix 系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
-
(1) fd = open("/dev/zero", O_RDWR);// 可以把所有的东西都扔进 /dev/null
-
(2) p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
strace 是个功能强大的Linux调试分析诊断工具,可用于跟踪程序执行时进程系统调用(system call)和所接收的信号,尤其是针对源码不可读或源码无法再编译的程序
- 用法:strace 可执行文件
无血缘关系进程间 mmap 通信
- 使用同一文件创建映射区
- 指定 MAP_SHARED
- 可多读端,多写端