进程的创建
frok函数添加进程
进程=内核的相关管理数据结构(task_struct +mm_struct+页表)+代码数据
特性:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork可能调用失败
- 因为一个系统的进程一定有一个上限,及用户线程到达上限
- 系统资源已满。
fork的返回值
- 子进程返回0
- 父进程返回子进程pid。
为了方便父进程对子进程标识,方便管理。
进程的终止
终止是在做什么?
释放曾经的代码和数据结构所占据的空间,释放内核数据结构(僵尸)task_sturct 停滞状态
进程退出码
return返回退出码(可返回自定义退出码)
退出码分为:
- 自定义退出码
- 系统默认退出码
都交由bash处理,但只存在最新的一个子进程的退出码。
父进程bash为什么要知道子进程的退出码(让用户知道)?
因为要知道子进程的退出情况,如果用问题就终止。
进程终止的3中情况。
跑完
1.代码正确,结果正确
2.代码跑完,结果不正确
都可以通过进程的退出码决定。
非跑完
3.代码异常终止
vs 编程运行时,操作系统发现你的进程做了不该做的事情,os杀掉进程。
异常本质:因为进程收到了os的信号,同时,一旦出现异常,退出码无意义。
一个进程退出,我们可以通过两个数字知道原因:退出码,退出信号(task_struct结束保留等待父进程),且父进程必须知道。
如何终止
main函数return,表示进程终止(非main,函数结束)
代码调用exit函数,表示进程终止(c语言库函数,刷新缓存区)
_exit(系统调用,也是exit的底层调用)
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
进程等待
结论:任何子进程,在退出情况下,一般必须要被父进程等待,否则僵尸,原因:
父进程通过等待,解决子进程的退出僵尸问题,回收系统资源(必要性)。
获取子进程的退出信息,知道子进程时因什么原因退出(充要性)。
如何等待(释放僵尸)
wait方法
#include
#include
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid方法
int *status;
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
- pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
- status:WIFEXITED(status):
若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options:
不休眠等待
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞(永久休眠)。
- 如果不存在该子进程,则立即出错返回。
等待父进程的一个子进程结束
子进程本身是一个软件,父进程本质实在等待某种条件就绪(阻塞等待)。
其中status变量是一个输出变量,他保存的是32位的退出码与退出信号的二进制合并。
WNOHANG等待方式:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{pid_t pid;pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__); return 1;}else if( pid == 0 ){ //child printf("child is run, pid is : %d\n",getpid()); sleep(5); exit(1);}else{int status = 0; pid_t ret = 0; do {ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待 if( ret == 0 ){printf("child is running\n");}sleep(1);}while(ret == 0);if( WIFEXITED(status) && ret == pid ){ printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status)); }else{ printf("wait child failed, return.\n"); return 1;}}return 0;
}
进程程序替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换原理
进程之间的替换是将代码与数据(目标数据代码)与源数据交换(在执行过程中掩盖源数据代码),从而实现进程之间的替换,其中堆栈中的数据会重新刷新。
常用法:
通过子进程执行该进程,其中子进程会执行内存上的一块新空间(写时拷贝),不影响父进程的使用。
ps: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[]);
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
命名理解
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
使用:
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径 execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径 execvp("ps", argv);// 带e的,需要自己组装环境变量 execve("/bin/ps", argv, envp);exit(0);
}
事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
下图exec函数族 一个完整的例子: