父进程先于子进程结束,子进程的父进程成为 init 进程。
子进程终止,父进程尚未回收,子进程残留的资源 (PCB) 存放于内核中,变成僵尸 (Zoombie) 进程。
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪一个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息, 然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 shell 中用特殊变量 $? 查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程
阻塞等待子进程退出
回收子进程残留资源
获取子进程结束状态(推出原因)
pid_t wait(int *status); 成功:清除掉的子进程ID; 失败:-1(没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2.释放用户空间分配的内存。内存的 PCB 仍然存在。其中保存该进程的退出状态(正常终止->退出值;异常终止->终止信号)
**可使用 wait 函数传出的参数 status 来保存进程的退出状态。**借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
WIFEXITED(status) 为非 0 -> 进程正常结束
WIFSIGNALED(status) 为非 0 -> 进程异常终止
WIFSTOPPED(status) 为非 0 -> 进程处于暂停状态
作用同 wait, 但可指定 pid 进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, int options);
特殊参数和返回情况:
参数 pid:
pid>0: 回收指定 ID 得子进程
pid==0: 回收和当前调用 waitpid 一个组任意子进程
pid==-1: 回收任意子进程 (相当于 wait)
pid<-1: 回收指定进程组内的任意子进程
参数 options:
注意:一次 wait 或 waitpid 调 用只能清理一个子进程,清理多个子进程应使用循环。
练习:父进程 fork 3 个子进程,三个子进程一个调用 ps 命令,一个调用自定义程序 1(正常),一个调用自定义程序 2(会段错误)。父进程使用 waitpid 对其子进程进行回收。
#include
#include
#include
#include
#include
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;
}
ulimit 是一个计算机命令,用于shell启动进程所占用的资源,参数形式有-H设置硬资源限制;-S 设置软资源限制;-a 显示当前所有的资源限制等。
Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。
在进程间完成数据传递需要借助操作系统提供特殊的方法,如:**文件、管道、信号、共享内存、消息队列、套接字、命名管道等。**随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方法有:
管道(使用最简单)
信号(开销最小)
共享映射区(无血缘关系)
本地套接字(最稳定)
管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道。有如下特质:
其本质是一个伪文件(实为内核缓冲区) // 伪文件不占用磁盘存储 s, b, c, p
由两个文件描述符引用,一个表示读端,一个表示写端。
规定数据从管道的写端流入管道,从读端流出。
管道的原理:管道实为内核使用环形队列机制,借助**内核缓冲区(4k)**实现。
管道的局限性:
数据自己读不能自己写。
数据一旦被读走,便不再管道中存在,不可反复读取。
由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
只能在由公共祖先的进程间使用管道。
常用的通信方式有:单工通信、半双工通信、全双工通信。
创建管道
int pipe(int pipefd[2]);
函数调用成功返回 r/w 两个文件描述符。无需 open, 但需手动 close。
规定:fd[0] -> r; fd[1] -> w。向管道文件读写数据其实是在读写内核缓冲区。
练习:利用 pipe 函数,子进程向父进程发送信息。
#include
#include
#include
#include
#include
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;
}
优点:实现手段简单
缺点
使用文件也可以完成 IPC,理论依据是,fork 后,父子进程共享文件描述符。也就共享打开的文件。
练习:编程测试,父子进程共享打开的文件。借助文件进行进程间通信。
#include
#include
#include
#include
#include
#include
#include
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 (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可以在不实用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过 mmap 函数来实现。
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
返回:
addr: 建立映射区的首地址,由 linux 内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot: 映射器权限 PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
fd: 用来建立映射区的文件描述符
offset: 映射文件的偏移(4k 的整数倍)
int munmap(void *addr, size_t length);
父子等有血缘关系的进程之间也可以通过 mmap 建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数 flags:
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区
MAP_SHARED: (共享映射) 父子进程共享映射区
练习:父进程创建映射区,然后 fork 子进程,子进程修改映射区内容,而后,父进程读取映射区内容,查验是否共享。
#include
#include
#include
#include
#include
#include
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)和所接收的信号,尤其是针对源码不可读或源码无法再编译的程序