一、进程创建
1. fork()函数
#include <unistd.h>
pid_t fork(void);
fork()函数从一个进程中创建一个新进程,新进程为子进程,原来的进程为父进程。
创建成功给父进程返回子进程的id,给子进程返回0。 创建失败则返回-1
来看段代码感受一下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
if(pid > 0)
{//father
printf("I am father pid=%d\n",getpid());
sleep(3);
}
else
{//child
printf("I am child pid=%d,ppid=%d\n",getpid(),getppid());
}
}
看看运行结果
我们可以发现if和else中的语句都被执行了,这是因为当一个进程调用fork()创建出了新的进程之后,就相当于有两个二进制代码相同的代码块,而且它们都运行到相同的地方,随后它们就可以执行自己对应的语句。
那么调用fork()函数之后,fork到底干了什么事情呢?
- 给新创建的进程分配一个内部的标识符,在内核中分配PCB
- 复制父进程的环境
- 为进程分配资源(代码,数据,堆栈)
- 父进程地址空间的内容也复制到新的进程空间中
- 将新进程放入就绪队列
fork()的调用场景:
- 一个父进程希望复制自己,使父子进程同时执行不同的操作
- 一个进程要执行一个不同的程序
fork()调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
我们可以根据 ulimit -a来查看系统允许创建的最大进程数
还可以在系统proc目录下查看:
2.vfork()函数
vfork()函数也是用来创建子进程的,但是和fork()函数有一定的区别
- fork是父子进程交替运行,vfork是子进程运行,父进程一直阻塞直到子进程结束(及子进程调用exit或_exit)
- fork实现了写实拷贝,vfork就算写也不拷贝
- vfork必须使用exit或_exit
- 虽然fork实现了写实拷贝但是性能也没有vfork高
- 每个系统上的vfork都有问题,不要使用
来看个vfork的例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int g_data=100;
int main()
{
pid_t pid;
pid=vfork();
if(pid==-1)
{
perror("vfork");
exit(1);
}
if(pid==0)
{
printf("child\n");
sleep(5);
g_data=110;
}
else
{
printf("parent\n");
sleep(1);
printf("parent g_data=%d\n",g_data);
}
exit(0);
return 0;
}
可以看出,子进程改变了父进程的变量值,因为子进程在父进程的地址空间中运行
二、进程等待
1.为什么要有进程等待
在之前的博客中我们谈过僵尸进程的例子,就是子进程退出,而父进程不管不顾,那么这个子进程就会变成僵尸进程,当一个进程变成僵尸进程也没有办法再去杀死这个僵尸进程。所以,我们可以让父进程通过进程等待的方式来获取子进程的退出信息并回收子进程的资源
可以通过下面两个函数来实现进程等待
2.wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
返回值:成功返回被等待进程的进程ID,失败返回-1
在这里要注意wait函数的参数,是一个输出型参数,是为了获取子进程退出状态,不关心则可以设置成为NULL
看个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork");
exit(1);
}
if(pid==0)
{//child
printf("this is child,pid=%d,ppid=%d\n",getpid(),getppid());
sleep(5);
exit(1);
}
else
{
printf("this is father,pid=%d,ppid=%d\n",getpid(),getppid());
pid_t ret = wait(NULL);
if(ret>0)
{
printf("wait success! ret=%d\n",ret);
}
}
}
运行过程中我们可以发现,开始父子进程都跑起来了,而只有在子进程退出之后父进程才会运行wait之后的代码。
wait()函数到底干了什么?
- 阻塞当前进程,直到有子进程退出才返回
- 回收子进程的残留资源
- 获得子进程的退出状态
对于第三点获得子进程的退出状态,我们再来把上面的代码稍作修改:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork");
exit(1);
}
if(pid==0)
{//child
printf("this is child,pid=%d,ppid=%d\n",getpid(),getppid());
sleep(20);
exit(1);
}
else
{
printf("this is father,pid=%d,ppid=%d\n",getpid(),getppid());
int st;
pid_t ret = wait(&st);
if(ret>0&&(st & 0x7f)==0)
{//正常退出
printf("wait success! ret=%d,exit code=%d\n",ret,(st>>8)&0xff);
}
else if(ret>0)
{//异常退出
printf("sig code=%d\n",st&0x7f);
}
}
}
我们在开启一个终端,将子进程kill掉让它异常退出
3.waitpid()
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
先来看看waitpid的第一个参数pid:
pid>0:等待某一个进程死亡,该进程的进程id就是参数的pid
pid=0:等待调用者进程所在进程组的任何一个子进程死亡
pid=-1:等待任何一个子进程死亡(相当于wait)
pid<-1:等待与pid绝对值相等的进程组的任何一个子进程死亡
第二个参数status:
- WIFEXITED(status):返回非零表示正常退出,返回零,表示不正常退出(查看进程是否是正常退出)
WEXITSTATUS(status):若WIFEXITED非零,获得退出码。(查看进程的退出码) - WIFSIGNALED(status):返回非零,表示由于信号导致退出
WTERMSIG(status):获得信号的值
注: status指出了子进程是正常退出还是被非正常结束的(一个进程也可以被其他进程用信号结束),以及正常结束时的返回值,或被哪一个信号结束或进程的退出码是多少等信息,这些信息都被放在整数的不同二进制位中,所以用常规的方法读取会非常麻烦,所以开发者就设计了一套专门的宏(macro)来完成这项工作。在这里不做演示
第三个参数options:
第三个参数通常被设置为0值或者WNOHANG,0值表示该进程等待的时候以阻塞方式等待。
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
返回值:
1. 当正常返回的时候waitpid返回收集到的子进程的进程ID;
2.如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
3.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
4.当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD.
先来看阻塞式等待的例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork");
exit(1);
}
if(pid==0)
{
while(1)
{
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
sleep(1);
exit(123);
}
}
else
{
printf("father pid=%d,ppid=%d\n",getpid(),getppid());
int status=0;
pid_t ret = waitpid(pid,&status,0);
if(ret>0)
{
printf("wait success! ret=%d,status=%d,exit code=%d,sig=%d\n",ret,status,(status>>8)&0xff,status&0xff);
}
}
}
再来试一下kill掉这个进程(将子进程中的exit注释掉):
有时候,我们想让父进程做一些事情,就有了非阻塞式等待:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
if(pid==-1)
{
perror("fork");
exit(1);
}
if(pid==0)
{
while(1)
{
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
sleep(5);
}
}
else
{
printf("father pid=%d,ppid=%d\n",getpid(),getppid());
int status=0;
do
{
pid_t ret=waitpid(pid,&status,WNOHANG);
if(ret==0)
{
printf("father doing something!\n");
sleep(1);
}
else if(ret>0)
{
printf("wait success! ret=%d,status=%d,exit code=%d,sig=%d\n",ret,status,(status>>8)&0xff,status&0xff);
break;
}
else
{
printf("wait failed!\n");
break;
}
}while(1);
}
}
由运行结果我们可以发现,父进程在等待过程中一直在做自己的事情。直到子进程退出
三、进程终止
进程终止有进程正常终止和异常终止
1.正常终止
- 从main返回,等效于调用exit
- 调用exit(exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。)
- 调用_exit或者_Exit
- 最后一个线程从其启动例程返回
- 最后一个线程调用pthread_exit
2.异常终止
- 调用abort
- 接到一个信号并终止
- 最后一个线程对取消请求做出响应
3._exit()函数
#include <unistd.h>
void _exit(int status);
参数status定义了进程的终止状态,父进程通过等待来获取该值。
需要注意的是,status虽然是int类型,但是仅有低八位可以被父进程使用,所以在_exit(-1),查看退出码,我们看到的是255.
4.exit()函数
#include <unistd.h>
void exit(int status);
exit函数其实还是调用了_exit函数,但是它还做了别的事情
- 执行用户通过atexit或on_exit定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("mmc");
sleep(3);
exit(1);
}
我们会发现三秒钟之后屏幕上输出mmc
要是改成_exit(1)呢?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("mmc");
sleep(3);
_exit(1);
}
我们会发现程序三秒钟之后直接退出
5.return退出
执行return n相当于执行exit(n),因为在main函数运行时,函数会将main的返回值作为参数传给exit。