目录
一、进程关键概念
二、进程创建实战
1、fork函数
2、fork创建一个子进程的一般目的:
3、fork函数实例:
4、fork的写时拷贝技术(COW)
三、进程退出
1、正常退出
2、异常退出
3、总结
四、僵死进程与孤儿进程
1、什么是僵死进程
2、什么是孤儿进程
3、僵尸进程与孤儿进程的区别
4、僵尸进程的危害
5、避免僵死进程的方法(三种方法)
一、进程关键概念
Q1:什么是程序,什么是进程,有什么区别?
A1:程序是静态的概念,gcc xxx.c -o pro执行后在磁盘中生成pro文件,叫做程序。进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程。
Q2:如何查看系统中有哪些进程?
A2:(1)使用ps -aux指令查看:实际操作中,配合grep来查找程序中是否存在某一个进程。
Q3:什么事进程标识符?
A3:每个进程都有一个非负整数表示的唯一ID,叫做pid。编程调用getpid()函数获取自身的进程标识符,getppid()获取父进程的进程标识符。
pid=0:称为交换进程(swapper),处理进程调度
pid=1:init进程,系统初始化和回收孤儿进程
Q4:什么是父进程,什么是子进程?
A4:进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念。
Q5:C程序的存储空间是如何分配的?
A5:
(1)正文段。这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是频繁执行的程序(如文本编辑器、C编译器和shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外而修改其自身的指令。
(2)初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。使变量带有其初值存放在初始化数据段中。
(3)非初始化数据段。通常将此段称为bss段,这一名称来源于一个早期的汇编运算符,意思是“block started by symbol”(由符号开始的块),在程序开始执行之前,内核将此段中的数据初始化为0或空指针。出现在任何函数外的C声明,使变量带有其初值存放在非初始化数据段中。
(4)栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次调用函数时,其返回地址以及调用者的环境信息(例如某些机器寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈,可以递归调用C函数。递归函数每次调用自身时,就使用一个新的栈帧,因此一个函数调用实例中的变量集不会影响另一个函数调用实例中的变量。
(5)堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段和栈之间。
二、进程创建实战
1、fork函数
终端输入man fork查看fork函数介绍、包含头文件和原形。
NAME
fork - create a child process
SYNOPSIS
#include <unistd.h>
pid_t fork(void);
2、fork创建一个子进程的一般目的:
(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
3、fork函数实例:
模仿服务器与客户端,客户端连接到来时子进程处理,父进程等待下个客户端到来。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
int data = 10;
while(1){
printf("please input a data\n");
scanf("%d",&data);
if(data == 1){
pid = fork();
if(pid > 0)
{
}
else if(pid == 0){
while(1){
printf("do net request,pid=%d\n",getpid());
sleep(3);
}
}
}
else{
printf("wait ,do nothing\n");
}
}
return 0;
}
fork函数调用成功,返回两次:一次返回到父进程,一次返回到子进程:
(1)返回值为0,代表当前进程是子进程。
(2)返回值为非负数,代表当前进程为父进程,返回值是子进程的id号
(3)调用失败,返回-1。
(4)父子进程谁先执行取决于系统调度。
4、fork的写时拷贝技术(COW)
(1)子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段(代码段)。
(2)由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时拷贝(Copy-On-Write, COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。
三、进程退出
1、正常退出
(1)main函数调用return
(2)进程调用exit(),标准c库
(3)进程调用_exit()或者_Exit(),属于系统调用
(4)进程最后一个线程返回
(5)最后一个线程调用pthread_exit
2、异常退出
(1)调用abort
(2)当进程收到某些信号是,如ctrl+c
(3)最后一个线程取消(cancellation)请求作出响应
3、总结
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit._exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
四、僵死进程与孤儿进程
1、什么是僵死进程
当子进程退出状态不被收集,就会变成僵死进程(僵尸进程)。一般情况下,程序调用exit(包括_exit和_Exit,它们的区别这里不做解释),它的绝大多数内存和相关的资源已经被内核释放掉,但是在进程表中这个进程项(entry)还保留着(进程ID,退出状态,占用的资源等等),你可能会问,为什么这么麻烦,直接释放完资源不就行了吗?这是因为有时它的父进程想了解它的退出状态。在子进程退出但还未被其父进程“收尸”之前,该子进程就是僵死进程,或者僵尸进程。如果父进程先于子进程去世,那么子进程将被init进程收养,这个时候init就是这个子进程的父进程。
所以一旦出现父进程长期运行,而又没有显示调用wait或者waitpid,同时也没有处理SIGCHLD信号,这个时候init进程就没有办法来替子进程收尸,这个时候,子进程就真的成了“僵尸”了。
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
pid = fork();
if(pid > 0)
{
while(1){
printf("cnt=%d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is chilid print, pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 3){
exit(0);
}
}
}
return 0;
}
在终端查看进程状态ps -aux|grep a.out,子进程退出,父进程没有接受子进程的退出状态,子进程编程僵尸进程。
2、什么是孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。
3、僵尸进程与孤儿进程的区别
回答这个问题很简单,就是爸爸(父进程)和儿子(子进程)谁先死的问题!如果当儿子还在世的时候,爸爸去世了,那么儿子就成孤儿了,这个时候儿子就会被init收养,换句话说,init进程充当了儿子的爸爸,所以等到儿子去世的时候,就由init进程来为其收尸。如果当爸爸还活着的时候,儿子死了,这个时候如果爸爸不给儿子收尸,那么儿子就会变成僵尸进程。
4、僵尸进程的危害
(1)僵死进程的PID还占据着,意味着海量的子进程会占据满进程表项,会使后来的进程无法fork。
(2)僵死进程的内核栈无法被释放掉(1K 或者 2K大小),为啥会留着它的内核栈,因为在栈的最低端,有着thread_info结构,它包含着 struct_task 结构,这里面包含着一些退出信息。
5、避免僵死进程的方法(三种方法)
(1)程序中显示的调用signal(SIGCHLD, SIG_IGN)来忽略SIGCHLD信号,这样子进程结束后,由内核来wai和释放资源。
(2)fork两次,第一次fork的子进程在fork完成后直接退出,这样第二次fork得到的子进程就没有爸爸了,它会自动被老祖宗init收养,init会负责释放它的资源,这样就不会有“僵尸”产生了。
(3)对子进程进行wait,释放它们的资源,但是父进程一般没工夫在那里守着,等着子进程的退出,所以,一般使用信号的方式来处理,在收到SIGCHLD信号的时候,在信号处理函数中调用wait操作来释放他们的资源。
例:调用wait函数,父进程等待子进程退出并收集子进程的退出状态
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int cnt = 0;
int status = 10;
pid = fork();
if(pid > 0)
{
wait(&status);
printf("child quit, child status = %d\n",WEXITSTATUS(status));
while(1){
printf("cnt=%d\n",cnt);
printf("this is father print, pid = %d\n",getpid());
sleep(1);
}
}
else if(pid == 0){
while(1){
printf("this is chilid print, pid = %d\n",getpid());
sleep(1);
cnt++;
if(cnt == 5){
exit(3);
}
}
}
return 0;
}
· wait(NULL); 不关心退出状态
· 如果子进程退出状态不被收集,则编程僵尸进程(不用wait函数)
· wait() 使调用者阻塞,等待子进程结束后,再开始父进程
· 参数非空就可以接收子进程结束返回参数(exit(3))
· WEXITSTATUS()这个宏,可以解析status,解析结果为3,如果不用宏解析它,status则是一个不确定整数
其余方法参考:https://blog.csdn.net/astrotycoon/article/details/39717143