目录
进程定义
进程与程序对比
进程分类
系统进程
用户进程
交互进程
批处理进程
守护进程
进程状态
进程组成
编辑正文段(text)和用户数据段
用户数据段
正文段
PCB进程控制块
进程标识信息
处理机状态
进程调度信息
进程控制信息
特殊进程
僵尸进程
定义
产生背景
危害
测试程序
解决办法
1.通过信号机制
2.fork两次
孤儿进程
定义
回收
测试程序
守护进程
定义
作用
分类
stand alone类型的守护进程
xinetd类型的守护进程
创建
背景知识:
创建守护进程的过程:
测试样例
kill
常用信号
kill -9 和 kill -15的区别
进程控制
进程创建
fork()
函数原型、参数说明和返回值
创建特性
fork 时的 资源继承说明
继承资源
不同属性
写实拷贝
子进程继承父进程已经打开的文件描述符
vfork()
函数原型
特征
clone()
函数原型
参数说明和返回值
应用实例
fork(),vfork(),clone()的区别
回收子进程的函数
wait函数
函数原型、参数说明、返回值
函数三个功能(同waitpid)
返回状态(status)获取方法
测试
waitpid函数
函数原型、参数说明、返回值
参数
返回值
wait(),waitpid()区别
使用案例(阻塞和非阻塞)
进程退出
正常退出
exit()和_exit()
函数原型
exit 和 _exit()对比
测试案例
异常退出
进程定义
进程是 Unix 和 Linux 系统中对正在运行中的应用程序的抽象,通过它可以管理和监视程序对内存、处理器时间和 I / O资源的使用。程序被触发后,执行者的权限与属性、程序的程序代码与所需数据等都会被加载内存中,操作系统并给予这个内存内的单元一个标识符 (PID),可以说,进程就是一个正在运作中的程序。 进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位 或者linux操作系统最小的资源管理单元。
进程与程序对比
进程分类
按照进程的功能和运行的程序分类,进程可划分为两大类:
系统进程
可以执行内存资源分配和进程切换等管理工作,而且该进程的运行不受用户的干预,即使是root用户也不能干预系统进程的运行。
用户进程
通过执行用户程序、应用程序或内核之外的系统程序而产生的进程,此类进程可以在用户的控制下运行或关闭。针对用户进程,又可以分为如下3类:
交互进程
由一个Shell终端其他的进程,在执行过程中,需要与用户进行交互操作,可以运行于前台,也可以运行于后台。
批处理进程
该进程是一个进程集合,负责按顺序启动其他的进程。
守护进程
守护进程是一直运行的一种进程,经常在Linux系统时启动,在系统关闭时终止。它们独立于控制终端且周期性地质学某种任务或等待处理某些发生的时间。例,httpd进程,crond进程等。
进程状态
一般操作系统将进程分为五个状态:
- 新建:新建表示进程正在被创建。
- 运行:运行是进程正在运行。
- 阻塞:阻塞是进程正在等待某一个事件发生。
- 就绪:就绪是表示系统正在等待CPU来执行命令。
- 结束:完成表示进程已经结束了系统正在回收资源。
Linux上进程有5种状态,这5中状态可以与一般操作系统的状态对应起来:
- 运行:正在运行或在运行队列中等待。
- 中断:休眠中, 受阻, 在等待某个条件的形成或接受到信号。
- 不可中断:收到信号不唤醒和不可运行, 进程必须等待直到有中断发生。
- 僵死:进程已终止, 但进程描述符存在,直到父进程调用wait4()系统调用后释放。
- 停止:进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行。
进程组成
- 正文段(text):存放程序代码。正文段具有只读的属性。
- 用户数据段(user segment):是进程在运行过程中处理数据的集合,它们是 进程直接进行操作的所有数据(包括全部变量在内),以及进程使用的进程堆栈。
- 系统数据段(system segment):存放着进程的控制信息,即进程控制块(PCB ,Processing Control Block),名字为task_struct的数据结构。
说明:
PCB(进程控制块) 结构体 task struct,负责管理进程的所有资源,它的成员 mm_struct 指向这个进程相关的内存资源,mm_struct指向一个结构体,包括:
栈 :给局部变量(自动变量)分配空间的地方
堆 :使用malloc、new… 分配的空间(也叫自由区)
BSS段
数据段
代码段:代码区是只读的,程序代码会被读入此区,程序执行期间执行的就是代码区中的代码。
正文段(text)和用户数据段
用户数据段
Linux系统把进程的数据段又划分成三部分:
- 用户栈区(供用户程序使用的信息区);
- 用户数据区(包括用户工作数据和非可重入的程序段);
- 系统数据区(包括系统变量和对换信息)
正文段
程序段是可重入的程序,能被若干进程共享。为了管理可共享的正文段,Linux设置了一张正文表,每个正文段都占用一个表目,用来指出该正文段在内存和磁盘上的位置、段的大小以及调用该段的进程数等情况。
PCB进程控制块
在Linux成功fork进程后,会在系统中创建一个task_struct(也称PCB, process control block),用来描述当前进程的状态、进程间关系、优先级和资源等信息。
标识符: 与进程相关的唯一标识符,用来区别正在执行的进程和其他进程。
状态: 描述进程的状态,因为进程有挂起,阻塞,运行等好几个状态,所以都有个标识符来记录进程的执行状态。
优先级: 如果有好几个进程正在执行,就涉及到进程被执行的先后顺序的问题,这和进程优先级这个标识符有关。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 程序代码和进程相关数据的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等。
记账信息: 包括处理器的时间总和,记账号等等。
PCB中包含4个部分
进程标识信息
用于唯一地标识一个进程,一个进程通常有两种标识符:
- 内部标志符: 由操作系统赋予每个进程的一个唯一的数字标识符,它通常为一个进程的序号,方便了系统使用。
- 外部标识符: 由创建者产生,是由字母和数字组成的字符串,为用户进程访问该进程提供方便。
- 为了描述进程间的家族关系,通常还设有父进程标识和子进程标识,以表示进程间的家族关系。
- 此外,还设有用户名或用户标识号表示该进程属于哪个用户。
处理机状态
处理机状态信息主要由处理机的各个寄存器内的信息组成。 进程运行时的许多信息均存放在处理机的各种寄存器中。其中 程序状态字(PSW) 是相当重要的,处理机根据程序状态寄存器中的PSW来控制程序的运行。
进程调度信息
- 进程状态: 标识进程的当前状态(就绪、运行、阻塞),作为进程调度的依据。
- 进程优先级: 表示进程获得处理机的优先程度。
- 为进程调度算法提供依据的其他信息:例如,进程等待时间、进程已经获得处理器的总时间和进程占用内存的时间等。
- 事件: 是指进程由某一状态转变为另一状态所等待发生的事件。(比如等待I/O释放)
进程控制信息
- 程序和数据地址: 是指组成进程的程序和数据所在内存或外存中的首地址,以便在调度该进程时能从其PCB中找到相应的程序和数据。
- 进程同步和通信机制: 指实现进程同步和通信时所采取的机制,如消息队列指针和信号量等,他们可以全部或部分存在PCB中。
- 资源清单: 列出了进程所需的全部资源 及 已经分配给该进程的资源,但不包括CPU.
- 链接指针: 它给出了处于同一队列中的下一个PCB的首地址。
特殊进程
僵尸进程
定义
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
产生背景
每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息:
- 包括进程号the process ID,
- 退出状态the termination status of the process,
- 运行时间the amount of CPU time taken by the process等)。
直到父进程通过wait / waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程.
危害
- 有个进程,它定期的产生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程。
- 僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程
- 方法:kill发送SIGTERM或者SIGKILL信号杀死该父进程
测试程序
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am child process.I am exiting.\n");
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
解决办法
1.通过信号机制
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
static void sig_child(int signo);
int main()
{
pid_t pid;
//创建捕捉子进程退出信号
signal(SIGCHLD,sig_child);
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am child process,pid id %d.I am exiting.\n",getpid());
exit(0);
}
printf("I am father process.I will sleep two seconds\n");
//等待子进程先退出
sleep(2);
//输出进程信息
system("ps -o pid,ppid,state,tty,command");
printf("father process is exiting.\n");
return 0;
}
static void sig_child(int signo)
{
pid_t pid;
int stat;
//处理僵尸进程
while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
printf("child %d terminated.\n", pid);
}
2.fork两次
原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main()
{
pid_t pid;
//创建第一个子进程
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//第一个子进程
else if (pid == 0)
{
//子进程再创建子进程
printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//第一个子进程退出
else if (pid >0)
{
printf("first procee is exited.\n");
exit(0);
}
//第二个子进程
//睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
sleep(3);
printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
exit(0);
}
//父进程处理第一个子进程退出
if (waitpid(pid, NULL, 0) != pid)
{
perror("waitepid error:");
exit(1);
}
exit(0);
return 0;
}
孤儿进程
定义
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
回收
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
注意:
- 任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。
- 如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。
- 如果父进程能及时处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
测试程序
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pid_t pid;
//创建一个进程
pid = fork();
//创建失败
if (pid < 0)
{
perror("fork error:");
exit(1);
}
//子进程
if (pid == 0)
{
printf("I am the child process.\n");
//输出进程ID和父进程ID
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("I will sleep five seconds.\n");
//睡眠5s,保证父进程先退出
sleep(5);
printf("pid: %d\tppid:%d\n",getpid(),getppid());
printf("child process is exited.\n");
}
//父进程
else
{
printf("I am father process.\n");
//父进程睡眠1s,保证子进程输出进程id
sleep(1);
printf("father process is exited.\n");
}
return 0;
}
守护进程
定义
Linux Daemon(守护进程)是运行在后台的一种特殊进程,并且不被任何终端产生的终端信息所打断。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。
注意:
一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。
作用
Linux 服务器在启动时也需要启动很多系统服务,它们向本地或网络用户提供了 Linux 的系统功能接口,直接面向应用程序和用户,而提供这些服务的程序就是由运行在后台的守护进程来执行的。
Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括:
- 系统日志进程syslogd、
- web服务器httpd、
- 邮件服务器sendmail
- 数据库服务器mysqld等。
分类
stand alone类型的守护进程
所谓 stand alone,指的是可独立启动的守护进程,这种类型的守护进程以下 2 大特点:
- 可以自动自动运行,而不需要利用系统其它机制来管理;
- 启动之后会一直占用内存和系统资源。
基于以上 2 个特点,这种守护进程就拥有一个非常突出的优点,即响应最快。stand alone 守护进程非常多,比如常见的 apache、mysql 等。
xinetd类型的守护进程
由一个统一的 stand alone 守护进程来负责唤起,这个特殊的守护进程被称为 super daemon。
之所以会引入这种机制,是因为 stand alone 会一直占用内存和系统资源,因此有人就提出了按需分配的这种概念。换句话说,当没有客户端要求的时候,xinetd类型的守护进程属于未启动状态,待有客户端要求服务时,super daemon 才会唤醒指定的 xinetd 守护进程。
xinetd 类型守护进程的缺点就是不能及时相应,但是优先很明显,
- 其一,由于 super daemon 负责唤醒各项服务,因此可以赋予super daemon安全管控的机制,这就类似网络防火墙的功能了;
- 其二,也是它的设置初衷,即客户端的联机结束后就关闭,不会一直占用系统资源。
创建
背景知识:
- 进程组:一个或多个进程的集合,进程组由进程组ID标识,进程组长的进程ID和进程组ID一致,并且进程组ID不会由于进程组长的退出而受到影响
- 会话周期:一个或多个进程组的集合,比如用户从登陆到退出,这个期间用户运行的所有进程都属于该会话周期
- setsid函数:创建一个新会话,并担任该会话组的组长,调用setsid函数的目的:让进程摆脱原会话,原进程组,原终端的控制
创建守护进程的过程:
- 创建子进程,父进程退出。子进程变成孤儿进程,然后由1号init进程收养
- 子进程创建新会话。调用setsid创建新的会话,摆脱原会话,原进程组,原终端的控制,自己成为新会话的组长
- 将当前目录改为根目录。正在运行的进程文件系统不能卸载,如果目录要回退,则此时进程不能做到,为了避免这种麻烦,以根目录为当前目录
- 重设文件权限掩码。子进程的文件权限掩码是复制的父进程的,不重新设置的话,会给子进程使用文件带来诸多麻烦
- 关闭不需要的文件描述符。子进程的文件描述符也是从父进程复制来的,那些不需要的文件描述符永远不会被守护进程使用,会白白的浪费系统资源,还可能导致文件系统无法结束。
测试样例
//https://www.cnblogs.com/yinbiao/p/11203225.html
#include <iostream>
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<semaphore.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
using namespace std;
#define max_file 65535
int main()
{
pid_t pc;
int fd,len;
char buf[]="this is a demo\n";
len=strlen(buf);
pc=fork();//第一步,创建子进程
if(pc<0)
{
cout<<"fork error"<<endl;
exit(1);
}
else if(pc>0)
{
exit(0);//第二步,父进程退出
}
else
{
setsid();//第三步,创建新会话
chdir("/");//第四步,将当前目录改为根目录
umask(0);//第五步,重新设置文件权限掩码
for(int i=0; i<max_file; i++)
{
close(i);//第六步,关闭不需要的文件描述符
}
while(1)
{
if((fd=open("/tmp/dameo.log",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
cout<<"open erro"<<endl;
exit(1);
}
write(fd,buf,len+1);
close(fd);
sleep(100);//每隔100s输出一句话到文件
}
}
}
kill
常用信号
15) SIGTERM:正常方式杀死进程:(这种方式可能会存在一些问题:进程的子进程可能会无法终止,并继续消耗系统资源)
9 )SIGKILL:强制杀死进程
2) SIGINT:中断信号 相当Ctrl+C:
18) SIGCONT:挂起进程重新执行
19) SIGSTOP:挂起进程,相当Ctrl+Z。
kill -9
和 kill -15
的区别
当使用kill -15时,系统会发送一个SIGTERM的信号给对应的程序。当程序接收到该信号后,具体要如何处理是自己可以决定的。这时候,应用程序可以选择:
- 立即停止程序
- 释放响应资源后停止程序
- 忽略该信号,继续执行程序
和kill -15相比,kill -9就相对强硬一点,系统会发出SIGKILL信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。
进程控制
#include <sys/types.h>
#include <unistd.h>
进程创建
fork()
- fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
- 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
函数原型、参数说明和返回值
pid_t fork(void)//创建一个子进程
//使用:
pid_t pid = fork();//定义了一个pid_t类型的变量pid,fork()函数返回一个进程号,这个进程号赋给了pid
- pid_t: 类似一个类型,就像int型一样,int型定义的变量都是整型的,pid_t定义的类型都是进程号类型。
返回值:
- >0 在父进程中,fork返回新创建的子进程的PID
- 0 在子进程中,fork返回0
- <0 如果出现错误,fork返回一个负值
换句话说,返回0代表子进程,返回正整数(子进程ID)代表父进程,如果返回-1代表调用失败,errno会被设置。
创建特性
- 调用一次,会有两个返回值
- 先返回哪个值,不确定, 用户可以通过延时函数,决定进程的执行先后顺序。
- 创建后,子进程复制父进程空间,这个空间子进程所独有的只有它的进程号、计数器和资源使用(子进程有自己的task_struct结构(复制来的)和pid)这样得到的子进程独立于父进程, 具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如: pipe,共享内存等机制
- 其他的和父进程空间一样,两个空间相互独立,互不干涉即使全局变量,在子进程中不会随父进程中变量的值的改变而改变。
- 从父进程复制出子进程,其实是复制出一个新的虚拟内存,因为我们的程序是运行在虚内存中的,所以我们也将虚拟内存空间称为进程空间,复制出的子进程(新的虚拟内存在空间)拥 有如下特点:父进程(父亲虚拟内存)的栈,堆,bss,. data, .ro.data 会从父进程中完全的复制 出一分给子进程(孩子虚拟内存),但是.text却是父子进程所共享。
fork 时的 资源继承说明
继承资源
- 实际UID、GID和有效UID,GID、附加GID
- 调用exec()时的关闭标志
- UID设置模式比特位、GID设置模式比特位
- 进程组号、会话ID、控制终端、环境变量
- 当前工作目录、根目录
- 文件创建掩码UMASK、文件长度限制ULIMIT
- 预定值, 如优先级和任何其他的进程预定参数, 根据种类不同决定是否可以继承,一些其它属性
不同属性
- 进程号,子进程号与任何一个活动的进程组号不同
- 子进程继承父进程的文件描述符或流时, 具有自己的一个拷贝;并且与父进程和其它子进程共享该资源
- 子进程的用户时间和系统时间被初始化为0
- 子进程不继承父进程的记录锁
- pending signals 也不会被继承
写实拷贝
fork函数创建出子进程后,子进程和父进程几乎是一样的,但是这对我们来说是没有什么意义的,我们需要将我们新的程序代码和数据复制到子进程中(复制出来的虚拟内存中),这个过程由exec函数实现,当需要向子进程中写入新的代码段时,子进程的.text段才被复制出新空间来并写入新的程序的代码,此时子进程的代码段会和父进程的代码段会慢慢分开,不再共享,这就是写时复制技术。
子进程继承父进程已经打开的文件描述符
//https://www.cnblogs.com/tshua/p/5756465.html
int main(void)
{
int fd = -1, ret = -1;
fd = open("file", O_CREAT|O_RDWR, 0664);
if(fd < 0) {
perror("open file is fail");
exit(-1);
}
ret = fork();
if(0 == ret){ //返回0是子进程
printf("in parent fd = %d\n", fd);
write(fd, "hello ", 6);
}
else if(ret > 0){ //返回正整数时父进程
printf("in child fd = %d\n", fd);
write(fd, "world\n", 6);
}
else if(-1 == ret) perror("fork is fail");
return 0;
}
我们发现这两个进程都向同一文件里面写数据,但是并没有出现数据相互覆盖的情况,所以我们可以猜测父子进程中的这两个文件描述符必然共享同一文件表中的文件读写指针,之所以world先写,这是由于父进程先执行导致的。
vfork()
不同于fork(父进程的内存数据copy到子进程),用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。
函数原型
pid_t vfork(void);
特征
- 子进程与父进程共享地址空间,子进程修改了某个变量,这将影响到父进程。很多程序在fork一个子进程后就exec一个外部程序,于是fork需要copy父进程的数据这个动作就变得毫无意义,子进程共享的 vfork,这样成本比较低。因此,vfork本就是为了exec而生。
- vfork创建子进程后,父进程会被阻塞,直到子进程调用exit或exec函数族(exec,将可执行文件载入到地址空间并执行。),不能调用return。
- 用vfork()创建的子进程必须显示调用exit()来结束,否则子进程将不能结束,而fork()则不存在这个情况。
- 调用vfork时,其中的代码要求尽可能少(因为容易不稳定)在调用exec或_exit之前与父进程数据是共享的,在它调用exec或_exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。
为什么return会挂掉但是exit不会
如果你在子进程中return,那么基本是下面的过程:
- 子进程的main() 函数 return了,于是程序的函数栈发生了变化。
- 而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup())
- 这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork调用返回 error)
好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装)
可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行。
但是!注意!如果你调用 exit() 函数,还是会有问题的,正确的方法应该是调用 _exit() 函数,**因为 exit() 函数 会 flush 并 close 所有的 标准 I/O ,这样会导致父进程受到影响。原因如下:
子进程调用的是 _exit 而不是 exit。exit 会实现冲洗标准 I/O 流,因此如果子进程调用的是 exit,那么会出 现两种情况:
- 如果函数库采取的唯一操作就是冲洗 I/O 流,那么此时得到的输出结果与子进程调用 _exit 时得到的输出结果是一样的;
- 如果该实现也关闭了 I/O 流,那么表示标准输出 FILE。对象的相关存储区将被清0.因为子进程借用了父进程的地址空间,因此当父进程恢复运行并调用 printf 的时候,printf会返回-1,即不会产生任何输出结果。
fork() vfork()的区别
- fork() 子进程拷贝父进程的数据段,代码段.
vfork() 子进程与父进程共享数据段. - fork() 父子进程的执行次序不确定.
vfork():保证子进程先运行。
clone()
#include <sched.h>//头文件
fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。
函数原型
实际上, clone系统调用是fork和pthread_create的通用形式,它允许调用者指定在调用进程和新创建的进程之间共享哪些资源。clone()的主要用途是实现线程:在共享内存空间中并发运行的程序中的多个控制线程。
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
参数说明和返回值
返回值是子进程的pid,出错的话返回-1,设置errno,并不建立子进程。
fn:函数指针
child_stack:为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值)
flags:用来描述你需要从父进程继承那些资源
- CLONE_VM ,父子进程运行在同一段内存
- CLONE_FS ,父子进程共享在root文件系统,当前工作目录以及umask信息
- CLONE_FILES ,父子信息共享文件描述符
- CLONE_SIGHAND ,父子进程共享在父进程上的信号处理器
- CLONE_PID ,父子进程具有相同的PID
**arg:**传给子进程的参数
应用实例
下面的例子是创建一个线程(子进程共享了父进程虚存空间,没有自己独立的虚存空间不能称其为进程)。父进程被挂起当子线程释放虚存资源后再继续执行。
#include <stdio.h>
#include <malloc.h>
#include <sched.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#define FIBER_STACK 8192
int a;
void * stack;
int do_something()
{
printf("This is son, the pid is:%d, the a is: %d\n", getpid(), ++a);
free(stack); //这里我也不清楚,如果这里不释放,不知道子线程死亡后,该内存是否会释放,知情者可以告诉下,谢谢
exit(1);
}
int main()
{
void * stack;
a = 1;
stack = malloc(FIBER_STACK);//为子进程申请系统堆栈
if(!stack)
{
printf("The stack failed\n");
exit(0);
}
printf("creating son thread!!!\n");
clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//创建子线程
printf("This is father, my pid is: %d, the a is: %d\n", getpid(), a);
exit(1);
}
parent和son中的a都为2;所以证明他们公用了一份变量a,是指针的复制,而不是值的复制。
fork(),vfork(),clone()的区别
- 系统调用fork()和vfork()是无参数的,而clone()则带有参数
- fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags决决定。(clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。 (void *child_stack,)也就是第二个参数,需要分配栈指针的空间大小,所以它不再是继承或者复制,而是全新的创造。)
- 执行顺序不同:
- fork不对父子进程的执行次序进行任何限制,fork返回后,子进程和父进程都从调用fork函数的下一条语句开始行,但父子进程运行顺序是不定的,它取决于内核的调度算法;
- vfork调用中,子进程先运行,父进程挂起,直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制;
- clone中由标志CLONE_VFORK来决定子进程在执行时父进程是阻塞还是运行,若没有设置该标志,则父子进程同时运行,设置了该标志,则父进程挂起,直到子进程结束为止
回收子进程的函数
#include <sys/types.h>
#include <sys/wait.h>
- 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
- wait或waitpid调用只能清理一个子进程,清理多个子进程需要用到循环
wait函数
等待任意一个子进程,这里指的是任意一个,调用该函数的进程的任意一个子进程结束,那么内核就会发送SIGCHILD信号给父进程通知它的某个子进程结束了,wait函数之前的阻塞会被SIGCHILD唤醒,并且获取到终止子进程的终止状态,将子进程的返回值填入到wait里面的status中。
函数原型、参数说明、返回值
pid_t wait(int *status);
- 返回值:成功返回终止进程的进程ID,失败返回非-1值(没有子进程),errno被设置。
- status: 整型指针。如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。
函数三个功能(同waitpid)
- 1、阻塞等待子进程退出
- 父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出
- 2、回收子进程残留资源
- 当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.
- wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.
- 如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.
- 3、 获取子进程结束状态(退出原因)
- 参数status用来保存被收集进程退出时的一些状态
返回状态(status)获取方法
请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数
- WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
- WIFSIGNALED(status) 为非0 → 进程异常终止,
- WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。
- WEXITSTATUS(status)当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
测试
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>//pid_t的定义
#include <unistd.h>
#include <wait.h>
#include <errno.h>
#include <stdlib.h>
void waitprocess();
int main(int argc, char * argv[])
{
waitprocess();
}
void waitprocess()
{
int count = 0;
pid_t pid = fork();//创建子进程,返回两个值,父进程返回子进程ID>0,子进程返回0
int status = -1;
if(pid<0)
{
printf("fork error for %m\n",errno );
}
else if(pid>0) //父进程空间
{
printf("this is parent ,pid = %d\n",getpid() );
wait(&status);//父进程执行到此,马上阻塞自己,直到有子进程结束。当发现有子进程结束时,就会回收它的资源。
}
else //子进程空间
{
printf("this is child , pid = %d , ppid = %d\n",getpid(),getppid());
int i;
for (i = 0; i < 10; i++)
{
count++;
sleep(1);//进入睡眠,暂时释放时间片,给其他线程
printf("count = %d\n", count);
}
exit(5); //非0表示子进程异常退出,返回至wait处,回收子进程资源
}
printf("child exit status is %d\n", WEXITSTATUS(status));//status是按位存储的状态信息,需要调用相应的宏来还原一下
printf("end of program from pid = %d\n",getpid() ); //父进程结束
}
waitpid函数
等待我们通过pid参数说明的某个进程的结束,当满足条件的子进程结束时,那么内核就会发送SIGCHILD信号给父进程通知它的有子进程结束了,wait函数之前的阻塞会被SIGCHILD唤醒,并且获取到终止子进程的终止状态;可以不阻塞。
函数原型、参数说明、返回值
pid_t waitpid(pid_t pid, int *status, int options);
参数
pid:等待的子进程ID
- pid > 0 时,仅仅等待进程id等于pid的子进程,无论其他已经有多少子进程运行结束退出,仅仅要指定的子进程还没有结束,waitpid就会一直等下去.
- pid = -1 时,等待不论什么一个子进程退出,没有不论什么限制,此时 waitpid 和 wait 的作用一模一样.
- pid = 0 时,等待统一进程组中的不论什么子进程,假设子进程已经增加了别的进程组,waitpid 不会对它做不论什么理睬.
- pid < -1 时, 等待一个指定进程组中的不论什么子进程,这个进程组的ID等于pid的绝对值。
- status:子进程的结束状态值(同wait)
options:提供了一些另外的选项来控制waitpid()函数的行为
- WNOHANG:若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0,若正常结束,则返回该子进程的ID
- WUNTRACED:返回终止子进程信息和因信号停止的子进程信息
- WCONTINUED:返回收到SIGCONT信号而恢复执行的已停止子进程状态信息
返回值
- 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
- 如果设置了选项
WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0; - 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
wait(),waitpid()区别
- waitpid函数比wait函数功能更强大,wait函数只能等待任意进程一个进程状态的改变,而且只能等待一种进程状态的改变,那就是进程终止,但是waitpid除了可以等待进程终止外,还可以等待进程是否暂停等其它的进程状态改变。
- wait函数一定是阻塞的,而waitpid是否阻塞则可被设置。
- wait函数等待的是任意一进程终止,waitpid则可等待指定进程的进程状态改变。
使用案例(阻塞和非阻塞)
阻塞
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid<0)
{
perror("fork");
exit(1);
}
if(pid==0)
{
printf("child is run,child pid is :%d\n",getpid());//getpid()得到的是当前进程的pid
sleep(2);
exit(5);
}
if(pid > 0 )//父进程
{
int status=0;
pid_t ret=waitpid(-1,&status,0);//pid = -1等待不论什么一个子进程退出.阻塞式等待2秒
printf("parent waiting..................\n");
if(WIFEXITED(status)&&(ret==pid))//获取status状态
{
printf("child is 2s succes.child exit code is :%d\n",WEXITSTATUS(status));
}
else if(ret>0)
{
printf("wait child failed.\n");
exit(1);
}
}
return 0;
}
非阻塞
//https://blog.csdn.net/dangzhangjing97/article/details/79745880
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid=fork();
if(pid<0)
{
perror("fork");
exit(1);
}
if(pid==0)
{
printf("child is run,child pid is :%d\n",getpid());//getpid()得到的是当前进程的pid
sleep(2);
printf("child run finally.\n");
exit(5);
}
if(pid>0)
{
int status=0;
pid_t ret=0;//记得需要声明ret
do
{
ret=waitpid(-1,&status,WNOHANG);//非阻塞式等待
if(ret==0)
{
printf("child is running,\n");
}
sleep(1);
}while(ret == 0);//ret!=0表示子进程结束
if(WIFEXITED(status)&&(ret==pid))
{
printf("child is 2s succes.child exit code is :%d\n",WEXITSTATUS(status));
}
else if(ret>0)
{
printf("wait child failed.\n");
exit(1);
}
}
return 0;
}
非阻塞式等待,就是谁先执行不一定,
1、父进程先执行的,然后让父进程睡眠1s
2、子进程执行,子进程执行到sleep(2)时
3、此时又去执行父进程,但是子进程还没有执行结束,故一直执行while循环
4、2s过后,又再次遇到sleep(1),又去执行子进程
5、这时子进程exit(5)就会结束
6、父进程继续完成自己的工作
进程退出
关闭进程打开的文件描述符,释放它占用的内存和其他资源
正常退出
- main函数调用return
- 在程序的任意位置调用exit函数;
- 在程序的任意位置调用_exit函数;
exit()和_exit()
函数原型
#include<stdlib.h>
void exit(int status);
/*exit()就是退出,传入的参数是程序退出时的状态码,0表示正常退出,其他表示非正常退出,一般都用-1或者1,标准C里有EXIT_SUCCESS和EXIT_FAILURE两个宏,用exit(EXIT_SUCCESS);*/#include<unistd.h>
void _exit(int status)
//立即终止一个进程。任何被本进程打开的文件描述符都会被关闭;该进程的父进程会收到SIGCHLD信号。
exit 和 _exit()对比
- _exit 函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核的各种数据结构;exit 函数则在这些基础上做了一些小动作,在执行退出之前还加了若干道工序。
- exit() 函数与 _exit() 函数的最大区别在于exit()函数在调用exit 系统调用前要检查文件的打开情况,把文件缓冲区中的内容写回文件。也就是图中的“清理I/O缓冲”。简单点说:exit先刷新流数据,将文件缓冲区的内容写回文件,可以保证数据的完整性,_exit会将数据直接丢失
printf()、fopen()、fread()、fwrite()都在此列,它们也被称作”缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符和文件结束符EOF), 再将缓冲区中的 内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特 定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
测试案例
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t result;
result = fork();
if(result<0)
perror("fork");
if(result == 0)
{
printf("This is _exit test\n");
printf("This is the _exit content in the buffer");
_exit(0);
}
else
{
printf("This is exit test\n");
printf("This is the exit content in the buffer\n");
exit(0);
}
return 0;
}
结果分析:
子进程中运行_exit(0)并未将Thisis the content in the buffer打印出来,而父进程中运行的exit(0)将Thisis the exit content in the buffer打印出来了。
说明,exit(0)会在终止进程前,将缓冲I/O内容清理掉,而_exit(0)是直接终止进程,并未将缓冲I/O内容清理掉,所以不会被打印出来。
异常退出
- 自杀:自己调用abort函数,自己给自己发一个信号将自己杀死,杀人凶器是信号;
- 他杀:由别人发一个信号,将其杀死,杀人凶器也是信号,这些信号是由其它进程或内核才生的(kill信号)