文章目录
引言
我们知道在linux系统中,一个父进程创建了一个子进程,此时子进程如果退出了,他就会保持僵尸状态,而僵尸状态的进程是有弊端的,虽然已经退出,但是
还在消耗内存资源。linux系统对内存的管理当然是相当的严格高效的,绝对是不允许这种浪费资源的事情发生的,那么linux系统会对僵尸状态的进程进行怎
么样的处理呢?
在解决这个问题之前,我们先了解一下预备知识
一,fork()
- 函数fork()是用来启动另一个进程的函数,启动起来的进程称为子进程,在调用fork()函数的程序称为父进程
- 子进程的大部分数据都是拷贝于父进程的,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1. 常规用法
- 一个进程要执行不同的程序。eg:子进程从fork()返回后,调用exec()。
- 一个父进程希望复制自己,使父子进行不同的代码段。eg:父进程等待客户端请求,子进程来处理请求。
2. 它和写时拷贝的关系
上面说道,子进程的大部分数据都是来源于父进程的,在linux这个对内存要进行高效利用的系统中,子进程的创建显然不会对他从父进程里继承的全部数据马上分配内存空间,这时系统会先给子进程和父进程一样的虚拟地址,当子进程真正需要修改数据时系统才会进行拷贝一块内存给子进程用,这就是写时拷贝。
二,进程终止
三种情况
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止(本质:因为某些原因,收到操作系统的信号)
1. 退出码
我们在学习C语言时,是不是常常在main函数
里面return 0
,这其中的0
就是我们学习的退出码,0是众多退出码的一种,表示程序代码正常终止。
我们来看看退出码的数量和含义:
在C语言中,有一个专门打印退出码含义的函数strerror()
,下面我们通过代码来演示
#include<stdio.h>
#include<string.h>
int main()
{
int i = 0;
for ( i = 0; i < 150; ++i)
printf("%d:%s\n",i,strerror(i));
return 0;
}
从图中我们可以看出,一共有134个退出码,常用的是0
表示正常退出,在linux操作系统下,我们可以通过echo $?
来查询上一个执行完的程序的退出码。
第一个echo $?
获取到的退出码是刚刚执行的程序是正常退出的,第2,3个都是系统指令返回的退出码,第4,5表示echo $?
自身执行成功返回的退出码。
2. exit()和_exit()的关系
我们先看关于exit()和_exit()的两段代码,同过比较,来学习一下两者的相同点和不同点。
- exit()
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s",strerror(0));
exit(2);
printf("\n");
return 0;
}
- _exit()
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("%s",strerror(0));
exit(2);
printf("\n");
return 0;
}
- 相同点:exit和_exit都可以在任何地方正常退出程序,并返回退出码。eg:上述代码就返回了2,并都没有执行
printf("\n");
.
-不同点:exit()
除了会正常退出程序外,他还会清理函数,刷新缓冲区,_exit()
则直接正常退出程序。
底层可以这么理解,exit 调用 _exit
exit(int code)
{
关闭文件,清理函数;
刷新缓冲区;
_exit();
}
三,进程等待
通过上面的知识了解,现在我们来解决开头的问题:父进程如何对僵尸进程进行处理
1. 什么是进程等待
父进程为了拿到子进程的运行信息结果和对子进程进行资源回收的等待过程
2. 为什么要进程等待
- 我们知道子进程结束后,如果父进程不做任何处理,那么子进程就会变成僵尸状态,而僵尸状态的进程刀枪不入,
kill -9
也杀不死它,所以会造成资源浪费,所以需要父进程调用wait() / waitpid()
来回收子进程的资源。- 获取子进程的结果。
- 保证时序。让子进程先退出,父进程后退出。
3. 进程如何等待
1. wait()
父进程执行到wait()
时,就进入阻塞状态,一直等待子进程的结束。便于观察,我多加了几个sleep()
。代码如下:
void test_wait()
{
pid_t pid = fork();
if(pid == 0)
{
int i = 5;
while(i--)
{
printf("%d,子进程,pid=%d\n",i,getpid());
sleep(1);
}
exit(0);
}
sleep(6);
printf("等待wait回收子进程资源...\n");
wait(NULL);
printf("子进程终于结束了,我是父进程,获取到子进程的pid=%d\n",pid);
sleep(2);
}
红色框:父子进程一并运行。
蓝色框:子进程结束,进入僵尸状态,等待父进程用wait()
来进行资源回收。
黄色框:子进程的僵尸状态消失,只剩父进程在运行。
ps:这里解释一下为什么父子进程全程都是S+(阻塞状态)。根据代码,如果去掉
sleep()
,其中在红色框时,子进程应该是R+(运行状态),父进程是S+,因为父进程调用了wait()
,需要等待子进程运行完毕才会继续下面代码的执行。后面蓝色和黄色框框父进程是R+。
2. waitpid()
来看看waitpid()的形式参数
有三个,其中第二个和wait(*status)
一样,上面没说,那么现在来学习一下。
- pid:子进程的pid,-1表示任意子进程
- status:一种状态码(一会就详谈)
- options:子进程状态。常用的:0表示阻塞状态,WNOHANG表示非阻塞状态
wait(NULL)
相当于waitpid(pid, NULL,0)
,pid表示子进程的pid。可以简单理解为waitpid()
是wait()
的升级版。
status
status的大小为4个字节,有32位,我们探究其后16位。
现在我们通过代码来看看
void test_wait()
{
pid_t pid = fork();
if(pid == 0)
{
int i = 5;
while(i--)
{
printf("%d,子进程,pid=%d\n",i,getpid());
sleep(1);
}
exit(3);
}
sleep(6);
printf("等待wait回收子进程资源...\n");
int status = 0;
wait(&status);
printf("子进程终于结束了,我是父进程,获取到子进程的pid=%d\n",pid);
printf("退出码:%d,终止信号:%d\n",(status>>8)&0xff,status&0x7f);
sleep(2);
}
- 这里3是子进程退出的返回值,我们用了exit(3)来显示表示,终止信号0表示正常运行,没有发生异常。
- status的引入可以让我们更好了解到子进程的运行信息。
- waitpid()/wait() 通过修改子进程的pcb来返回status的信息。
status其实可以用系统提供的宏定义WIFEXITED(STATUS)
来判断是非是正常退出,用WIFSTATUS(STATUS)
来获取进程退出码
options
在学习wait()的时候,我们有提到过,子进程在运行时,父进程在阻塞,这是对的,经过我们验证的。因为**wait()
是默认让父进程进行阻塞状态**,而waitpid()
却有options这个参数来控制父进程的状态。
下面我们来看看
void test_wait()
{
pid_t pid = fork();
if(pid == 0)
{
int i = 5;
while(i--)
{
printf("%d,子进程,pid=%d\n",i,getpid());
sleep(1);
}
exit(0);
}
//sleep(6);
//printf("等待wait回收子进程资源...\n");
int status = 0;
waitpid(pid, &status, WNOHANG);
int i = 0;
for(i = 0; i < 5; ++i)
{
printf("我是父进程,我先做自己的事情...\n");
sleep(1);
}
printf("子进程终于结束了,我是父进程,获取到子进程的pid=%d\n",pid);
printf("退出码:%d,终止信号:%d\n",(status>>8)&0xff,status&0x7f);
sleep(2);
}
可以看到,父进程并没有等待子进程运行完才执行自己的事情,而是并发执行,两种执行顺序没有先后之分,由操作系统来调度。