目录
一、fork函数
1.进程:
2.fork函数:
3.写时拷贝
4.fork常规用法
5.fork调用失败的原因
二、进程终止
1.终止是在做这么?
2.进程终止的3种情况
3.如何终止
三、进程等待
四、进程程序替换
1.替换原理
2.原理
3.将代码改成多线程
4.认识这些函数
1.execl
2.execv
3.execlp/execvp
4.execle/execvpe
一、fork函数
1.进程:
内核的相关管理数据结构(task_struct + mm_struct + 页表) + 代码和数据
2.fork函数:
在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
fork函数返回值:子进程返回0,父进程返回的是子进程的pid。
为什么fork返回值是这样? 为了让父进程对子进程进行标识,知道子进程的pid,可以管理子进程。
3.写时拷贝
1.概念:
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
虚拟地址一样,物理地址却不一样。
4.fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
5.fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
二、进程终止
1.终止是在做这么?
释放曾经的代码和数据所占据的空间
释放内核数据结构
2.进程终止的3种情况
#include <stdio.h>
#include <unistd.h> int main()
{ printf("i am a process! pid:%d, ppid:%d\n",getpid(), getppid()); sleep(3); return 100;
}
~
echo:内建命令,打印的都是bash内部的变量数据
? : 父进程bash获取到的,最近一个子进程退出的退出码
退出码 : 告诉关心方(父进程),我把任务完成的怎么样了
0:成功 !0 : 失败
1,2,3,4,5....不同的非0值,一方面表示失败,一方面表示失败的原因
for(int errnum = 0; errnum < 240; ++errnum)printf("%d -> %s\n",errnum, strerror(errnum)); //可以打印出每个数字代表的信息
为用户负责,可以了解错误信息
#include <stdio.h>
#include <unistd.h>
#include <string.h>enum
{success=0,Div_zero,Mod_zero,
}; const char* codeToerrstring(int code)
{ switch(code) { case success: return "success"; break; case Div_zero: return "Div_zero"; break; case Mod_zero: return "Mod_zero"; break; default: return "unknow error"; break; }
} int exit_code = 0;int Div(int x, int y)
{if(x == 0){exit_code = Div_zero;return -1;}else{exit_code = success;return x / y;}
}int main()
{int result = Div(0,2);printf("result = %d , %s\n",result, codeToerrstring(exit_code));return exit_code;
}
代码跑完结果正不正确,可以通过进程的退出码来决定!
出错原因:系统 或者 自定义退出码。
代码执行时,出现异常,提前退出。
进程出异常,本质是进程收到OS发给进程的信号,提前终止
一旦出现异常,退出码便没有意义。
1.先确定是否异常
2.不是异常,就一定是代码跑完了,看退出码就行
衡量一个进程退出,我们只需两个数字:退出码,退出信号!
退出码 | 退出信号 | 释义 |
0 | 0 | 运行成功 |
!0 | 0 | 运行结果不对 |
!0 | !0 | 进程异常 |
0 | !0 |
3.如何终止
1> main函数return,表示进程终止(非main函数 - return,函数结束)。
2> exit() -- C库函数。 代码调用exit函数。注:我们代码的任意位置调用exit,都表示进程终止。
3> _exit() -- system call(系统调用) , exit会在进程退出时,冲刷缓冲区,而_exit不会。其它不变
注:exit底层调用_exit,目前我们所说的缓冲区,不是内核缓冲区。
三、进程等待
结论:任何子进程,在退出的时候,一般必须要被父进程进行等待。进程在退出的时候,如果父进程不管不顾,退出进程,子进程就会进行状态Z(僵尸进程),导致内存泄漏
why?
1.父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)
2.获取子进程的退出信息,知道子进程是什么原因退出的。(可选的功能)
怎么办?
wait/waitpid
pid_t wait(int *status),等待父进程时,任意一个子进程退出,即等待成功。 等待成功时,返回子进程的pid。
如果子进程没有退出,父进程其实一直在进行阻塞等待!
pid_t waitpid(pid_t pid, int *status, int option)
返回值:当正常返回的时候waitpid返回手机的子进程的进程id
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出
的子进程可收集,则返回0
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
参数:pid: Pid = 1,等待人一个子进程。与wait等效
Pid = 0,等待其进程ID与pid相等的子进程。
int status (32位)
退出信息:1.输出型参数
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>void ChildRun()
{ int cnt = 5; while(cnt--) { printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid()); sleep(1); }
} int main()
{ printf("i am father, pid:%d, ppid:%d\n", getpid(), getppid()); pid_t id = fork(); if(id == -1) return 1; if(id == 0) { //child ChildRun(); printf("child quit...\n"); exit(1); //异常退出} sleep(7); //parent //pid_t rid = wait(NULL); int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { printf("wait successful, rid:%d\n", rid); } else { printf("wait file!\n"); } sleep(3); printf("father quit,status:%d, child quit code:%d, child quit signal:%d\n",status, (status >> 8) & 0xFF, status & 0x7F); return 0;
}
如果子进程是一个死循环,这时我们通过kill -9 3925发出一个信号终止子进程的话,就会得到
如果我们在子进程中加入一个野指针异常,比如:
void ChildRun()
{ int cnt = 5; int* p =NULL; while(1) { printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid()); sleep(1); *p =100; //野指针 }
}
上述status使用了为运算的方式,对于运用不是很友好
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>void ChildRun()
{int cnt = 5;//int* p =NULL;while(cnt--){printf("i am child, cnt:%d pid:%d, ppid:%d\n", cnt, getpid(), getppid());sleep(1);//*p =100;}
}int main()
{printf("i am father, pid:%d, ppid:%d\n", getpid(), getppid());pid_t id = fork();if(id == -1) return 1;if(id == 0) {//childChildRun();printf("child quit...\n");exit(123);}//fatherwhile(1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);//non blockif(rid == 0){printf("child is running, father check next time!\n");usleep(1000);//DoOtherThing();}else if(rid > 0){if(WIFEXITED(status)){printf("child quit success! child quit code:%d\n",WEXITSTATUS(status));} else{printf("child quit unsuccess! child quit code:%d\n",WEXITSTATUS(status));}break;}else{printf("wait filed!\n");}}return 0;
}
WNOHANG:非阻塞式等待
四、进程程序替换
1.替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
#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 execvpe(const char *path, char *const argv[], char *const envp[]);
1.execl
#include <stdio.h>
#include <unistd.h>int main()
{printf("begin execl...\n");execl("/usr/bin/ls", "ls", "-a", "-l", NULL);printf("end execl...\n");return 0;
}
2.原理
进程的程序替换
没有创建新的进程
站在被替换进程的角度,本质上就是这个程序被加载的内存中了
exec*系列的函数,执行完毕后,后续代码不见了,因为被替换了
execl函数的返回值不关心,因为替换成功,将不会向后继续运行,只要继续向后运行,则一定是运行失败了
类似“夺舍”。
3.将代码改成多线程
fork创建子进程,让子进程自己去替换 ,父进程wait
既可以让子进程完成了任务,父进程还不受影响。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("begin execl...\n");pid_t id = fork();if(id == 0){//childsleep(2);execl("/usr/bin/ls", "ls", "-a", "-l", NULL);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0); //blockif(rid > 0){printf("father wait success! child exit code:%d\n", WEXITSTATUS(status));}printf("end execl...\n");return 0;
}
父子进程具有独立性,当代码数据改变时,则子进程会发生写时拷贝。
4.认识这些函数
1.execl
l:list 列表。 path:我们要执行的程序,需要带路径。
例:ls -a -l execl(“/usr/bin/ls”,“ls”,“-a”,“-l”,NULL) ,结尾以NULL结束
2.execv
v:vector
int main()
{printf("begin execl...\n");pid_t id = fork();if(id == 0){//childsleep(2);char *const argv[] = {"ls","-a","-l","--color",NULL,};execv("usr/bin/ls", argv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0); //blockif(rid > 0){printf("father wait success! child exit code:%d\n", WEXITSTATUS(status));}printf("end execl...\n");return 0;
}
3.execlp/execvp
p:用户可以不传要执行程序的路径(但是文件名要传),直接告诉它要执行谁就行。
查找这个程序,系统会自动在系统环境变量PATH中查找
例:execlp("ls", "ls", "-a", "-l", NULL); execvp("ls", argv);
4.execle/execvpe
e:environment
回到execl,我创建一个程序,并且要在testexec.c中运行它,并且打印它的pid
它们的pid均为22166,证明没有创建新的进程。
envp:整体替换所有的环境变量
1.用全新的自己写的给子进程
2.用老的环境变量给子进程,enverion
3.老的环境变量稍微修改,给子进程 putenv("");
5.真正的系统调用:execve
其它exec函数都是execve的封装,底层都是调用的这个函数