前言:整个操作系统都在围绕进程这一概念具体展开,所以对于进程的控制就显得十分重要,这篇文章主要讲述以下几点:
1. 进程创建
2. 进程退出
3. 进程等待
4. 进程程序替换
进程创建
在操作系统中,对于父子进程的概念非常重要,必要linux自带的bash,对于你在命令行输入的一些指令,它是不会自己去处理你这些请求的,而是通过创建子进程去处理,它只需要知道子进程返回的消息就好了。
为什么要基于这样的父子进程关系呢?试想一下,我们和操作系统打交道是通过shell内建的bash,如果用户的什么请求都经由bash去亲自执行,那么一个bash也不够用啊,其次,如果一旦请求中出了问题,那么bash挂掉的话,谁来帮我们向操作系统传达我们的请求呢?
基于上面提出的种种问题,就引出进程创建子进程的必要性了。
进程的创建方式:
- pid_t fork(void);
- pid_t vfork(void);
认识fork函数
pid_t fork(void)
返回值:pid_t其实就是一个整型,typedef成pid_t只是为了一眼看上去知道这是一个进程号
子进程中返回值为0.
父进程中返回操作系统给子进程分配的pid号。
fork失败返回-1
进程调用fork,当控制权限转移到内核中的fork代码后:
- 分配新的内存块和数据结构给子进程
- 将父进程的大多数数据结构拷贝至子进程
- 添加子进程到系统进程列表
- fork返回,操作系统进行进程调度
当一个进程fork出一个子进程后,就有两个二进制代码相同的进程,并且运行到相同的地方,但每个进程都将开始执行自己的代码。
如下:
int main()
{
printf("Before fork: pid is:%d\n",getpid())//getpid函数为获取进程pid
pid_t id = fork();
if(id < 0)
{
perror("fork error");
exit(1);
}
printf("After fork:pid is %d\n",getpid());
return 0;
}
这里需要注意的是,先执行子进程还是父进程完全取决于操作系统的进程调度器决定。
fork失败的原因:
- 系统中的进程数达到了上限
- 系统的内存不足
- 系统不支持,如Windows不支持fork
认识vfork函数
对于vfork来说,其他的都是fork函数用法一样,只要记住最重要的两个特性就好。
- 子进程一定先于父进程执行。
- 子进程调用exec或者exit之后父进程才能执行
进程终止
进程退出的场景:
- 代码运行完,结果正确
- 代码运行完,结果不正确
- 代码异常终止
常见进程退出:
正常终止:
1. main函数返回
2. 调用exit函数
3. 调用_exit函数
exit函数和_exit函数的区别:
- exit会进行清理工作,如刷新缓冲区等,而exit直接退出程序
- _exit是系统调用,exit最终也会调用_exit。
异常终止
CTRL+C/kill -9
具体的程序退出部分,可见博客尾部的链接。
进程等待
进程等待是非常重要的,如果父进程对子进程不管不顾的话,那么可能会产生僵尸进程,从而造成内存泄漏。
并且作为父进程,创建子进程是让它完成一些任务的,总要知道它返回的结果,完成的怎么样。
wait函数
pid_t wait(int *status)//阻塞式等待
返回值:成功返回等待进程的pid,失败返回-1
参数:输出型参数,获取子进程的退出状态,不关心可以为NULL,该参数由操作系统初始化
status
所以查看的话,先查看低七位是否为0 ,如果是0代表程序正常退出,可以查看高八位具体的退出码。
如果低七为不为0,则代表信号终止,高八位就没有意义了,可以查看低七位的具体信号。
core dump是指进程终止时所记录的现场。
以下面的代码为实例:
#include<stdio.h>
#include<wait.h>
int main()
{
pid_t id = fork();
if(id > 0)
{
//father
int status = 0;
int ret = wait(&status);
if(ret > 0 && (status&0x7f) == 0)
{
//success
printf("child exit code:%d\n",(status>>8));
}
else
{
//signal exit
printf("signal code:%d\n",(status>>8)&0xff);
}
}
else if(id == 0)
{
//child
sleep(3);
exit(5);//子进程的退出码
}
else
{
perror("fork");
}
return 0;
}
当子进程正常退出时,会返回退出码。
运行结果:
接下来,我们直接kill -9掉该进程,结果应该返回9号信号,看如下运行结果:
我的Ubuntu是最新的,本应该返回9,但是操作系统将9这个数字,对应成第九个信号的名称显示出来。
下面是Linux下的信号:
waitpid函数
pid_t waitpid(pid_t pid,int *status,int option)//如果最后一个参数设置了WNOHANG就是非阻塞式等待
返回值:
- 正常返回收集子进程的进程ID
- 如果设置了选项WNOHANG,而调用waitpid发现没有已退出的子进程可以回收,则返回0,就是轮询等待的意思
- 如果调用中出错,返回-1,errno会被设置成相应的值以指示错误所在。
参数:
pid:
- pid = -1,等待任意一个子进程,与wait等效
- pid > 0,等待其进程ID与pid相等的进程
status:
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真(相当于上面的检查低7位为是否为0)
- WEXITSTATUS(status):若WIFIXITED非零,提取子进程退出码(相当于上面的查看高8位的退出码)
option:
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,则返回该进程的ID
需要注意的是:
- 如果子进程已经结束,调用wait/waitpid时,函数会直接返回,并且释放资源,获得子进程退出信息。(对应的场景就是子进程退出时,父进程在沉睡,如果这时父进程不予以处理子进程则会产生僵尸进程)
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
如果不存在该子进程,则立即出错返回
如下代码示例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id > 0)
{
//father
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1,&status,WNOHANG);//no-blocking
if(ret == 0)
{
printf("child is running\n");
}
sleep(1);
}
while(ret == 0);
if(WIFEXITED(status) && ret == id)
{
printf("wait child 3s success,child return code is:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed,return\n");
return 1;
}
}
else if(id == 0)
{
//child
printf("child is run ,pid is:%d\n",getpid());
sleep(3);
exit(1);
}
else
{
printf("%s fork error\n",__FUNCTION__);
return 1;
}
return 0;
}
进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程一般要调用一种exec函数以执行另一个程序。当进程调用exec函数族时,该进程的用户空间代码和数据完全被新的程序替换,从新程序的启动例程开始执行,调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。
exec函数族
#include<unistd.h>
int execl(const char *path,const char *arg,...);
int execlp(const char &file,const char *arg,...);
int execle(const char *path,const char &arg,...,char *const envp[]);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
int execve(const char *path,char *const argv[],char *const envp[]);
函数解释
- l(list):表示参数采用列表
- v(vector):表示参数采用数组
- p(path):带p的函数会自动搜索环境变量PATH
- e(env):表示自己维护环境变量
需要特别注意的时,
- 如果exec函数族调用成功,则从新程序的启动代码开始,所以没有返回值
- 如果调用失败,则返回-1
代码示例:
#include<stdio.h>
#include<unistd.h>
int main()
{
const *const argv[] = {"ls","-al",NULL};
char *const envp[] = {"PATH=/bin:/usr/bin",NULL};//环境变量
execl("/bin/ls","ls","-al".NULL);
//带p的函数,不必再给出全路径
execlp("ls","ls","-al",NULL);
//带e的,需要自己配置环境变量
execle("ls","ls","-al".NULL,envp);
execv("/bin/ls",argv);
//带p的,不需要给出全路径
execvp("ls",argv);
//带e的,需要自己配置环境变量
execve("/bin/ls",argv,envp);
exit(0);
}
虽然exec函数族有六个函数,但是只有execve函数是系统调用,其他几个函数最终都会调用execve函数。
基于程序替换,可以实现一个简单的shell。
exit函数详解http://blog.csdn.net/qq_36528114/article/details/71321390
实现一个简单的shell:http://blog.csdn.net/qq_36528114/article/details/72582588