目录
一.进程创建
1.fork函数
2.fork返回值
3.写时拷贝
4.fork常规用法
5.fork调用失败的原因
二.进程终止
1.进程退出场景
2.进程退出码
3.进程正确退出方法
4.进程异常退出
三.进程等待
1.进程等待的必要性
2.获取子进程的status
3.wait方法
4.waitpid方法
四.进程替换
1.替换原理
2.替换函数
3.函数解释
4.命名理解
五.一个简易的的shell程序
六.思考函数和进程之间的相似性
exec/exit就像call/return
进程调用fork,当控制转移到内核中的fork代码后,内核做:
注意: fork之后,父进程和子进程谁先执行完全由调度器决定
父:子 = 1:n ; 一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。
(2)为什么fork有两个返回值?
fork函数内部执行return语句之前,子进程就已经创建完毕了,父子进程都需要return返回。
数据是很多的,不是所有的数据立马被使用,且不是所有的数据都要进行拷贝。但是如果立马独立,就需要将数据全部拷贝,把本来可以在后面拷贝的,甚至不需要拷贝的数据,都拷贝了,就比较浪费时间和空间,所以拷贝的过程不是创建进程时立马做的。
(1) 为什么父进程创建子进程时父进程的数据段是只读的而不是可读的?
一旦父进程发现要创建子进程就将可读可写状态设置为只读,将来不管是父进程还是子进程一旦想往数据段写的时候立马会报错OS会识别到,识别到之后OS根据现在的创建完子进程要进行数据写入时,OS立马处理这个错误时就按照写时拷贝的方式处理,处理完毕之后,数据段的只读属性就被去掉了。
(2)为什么要进行写时拷贝? (写时拷贝是OS的内存管理,自动完成)
进程具有独立性(进程各自的PCB,地址空间,页表)。多个进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。
(3)为什么不在创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。
(4)子进程创建出来的时候会不会立马被调度?
不一定,所以OS在一定时间段内(子进程被创建到被调度),系统可用的内存变少了
(5)代码会不会进行写时拷贝?
90%的情况不会,但是不代表不能,(进程替换时代码写时拷贝)
(6)OS是很节省空间的
(1)一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。
(2)一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数,进程替换。
(1)系统中有太多的进程,内存空间不足,子进程创建失败。
(2) 实际用户的进程数超过了限制,子进程创建失败。
(1)代码运行完毕,结果正确。
(2)代码运行完毕,结果不正确。
(3)代码异常终止(进程崩溃)。
(1)main函数的退出的时候,return 数字叫做进程的退出码!
[gsx@VM-0-2-centos temp]$ echo $? //查看最近一次进程退出的退出码
(2) 程序运行时main函数是入口(用户级别), main函数也是函数,它也是被调用的,被谁调用的? OS直接/间接通过系统调用接口调用main函数,返回值最终会返回给OS
(3)为什么必须给main函数设置返回值?
程序运行起来,加载内存,形成进程;目的: 完成某种工作,工作是由人发起的,需要知道工作完成的如何,结果怎样。
(4)为什么main的return一般写成0 ?
0在函数设计中,一般代表正确非零错误,!0代表失败,每一个数字代表一种失败原因。
注意: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。
(1)从main函数返回,return
只有main函数中的return代表进程退出
(2)exit函数
exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:
(3)_exit函数
_exit函数退出进程的方法并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作
(4)return、exit和_exit之间的区别与联系
①区别
只有main函数当中的return才退出进程,其他函数中return叫终止函数,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。
exit会释放进程曾经占用的资源,比如:缓冲区;_exit直接终止进程,不会做任何收尾工作!
②联系
执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数,使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程
(5)进程终止了,操作系统做了什么?
释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除
(1)向进发送信号导致进程异常退出。
(2)代码错误导致进程运行时异常退出(野指针访问,除0错误: 本质还是发送信号杀掉进程)
进程异常退出了,退出码还有意义吗? 没有任何意义,最大的意义是为什么异常了。
(1)子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
(2)进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
(3)对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
(4)父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
在status的低16比特位当中,如果进程正常终止,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志
(5)根据status获取进程的退出码/终止信号
①位操作
exitcode = (status >> 8) & 0xFF; //退出码
exitsignal = status & 0x7F; //终止信号
②OS的两个宏来获取退出码和退出信号。
函数: pid_t wait(int*status);返回值:成功返回被等待进程pid ,失败返回 -1 。参数: 输出型参数,获取子进程退出状态, 不关心则可以设置成为 NUL特点: 等待任意子进程
#include
#include
#include
#include
#include
int main()
{
//wait测试代码
pid_t id = fork();
if(id == 0){ //child
int count =0 ;
while(count < 5){
printf("I am child , pid:%d ppid:%d \n",getpid(), getppid());
count++;
sleep(1);
}
exit(10);
}
else{ //father
printf("I am father , pid:%d ppid:%d \n",getpid(), getppid());
int status = 0;
pid_t ret = wait(&status);
if(ret >= 0){
printf("wait success!,%d\n",ret);
printf("位操作:\n");
printf("exit code:%d\n" ,(status>>8)&0xff);
printf("exit singal:%d\n" , status&0x7f);
printf("通过宏判断:\n");
if(WIFEXITED(status)){ //exit normal
printf("exit code:%d \n" , WEXITSTATUS(status));
}
}
printf("Father run done ...\n");
sleep(5);
}
return 0;
}
使用监控脚本对进程进行实时监控,查看进程的状态和退出情况:
[gsx@VM-0-2-centos temp]$ while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep;echo "--------------------";sleep 1;done
当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。
函数: 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: 输出型参数③options : 当设置为 WNOHANG 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。特点:可以等待指定进程
(1)单进程阻塞式等待
1 #include
2 #include
3 #include
4 #include
5 #include
6
7
8 int main()
9 {
10 pid_t id = fork();
11 if (id == 0){ //child
12 int count = 5;
13 while (count--){
14 printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
15 sleep(1);
16 }
17
18 exit(10);
19 }
20
21 //father
22 int status = 0;
23 pid_t ret = waitpid(id, &status, 0);
24 if (ret >= 0){
25 //wait success
26 printf("wait success...\n");
27 if (WIFEXITED(status)){
28 //exit normal
29 printf("exit code:%d\n", WEXITSTATUS(status));
30 }
31 else{
32 //signal killed
33 printf(" siganl %d\n", status & 0x7F);
34 }
35 }
36
37 printf("father done ...\n");
38 sleep(3);
39
40 return 0;
41 }
(2)单进程非阻塞轮询式等待
①当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。
②子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待;通过传递参数WNOHANG我们轮询式检测子进程是否退出,没有退出就做自己的事情。
③父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。
1 #include
2 #include
3 #include
4 #include
5 #include
6
7 int main()
8 {
9 pid_t id = fork();
10 if (id == 0){ //child
11 int count = 3;
12 while (count--){
13 printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
14 sleep(3);
15 }
16
17 exit(10);
18 }
19
20 //father 轮询检测
21 while (1){
22 int status = 0;
23 pid_t ret = waitpid(id, &status, WNOHANG);
24 if (ret > 0){
25 printf("wait child success...\n");
26 printf("exit code:%d\n", WEXITSTATUS(status));
27 break;
28 }
29 else if (ret == 0){
30 printf("father do other things...\n");
31 sleep(1);
32 }
33 else{
34 printf("waitpid error...\n");
35 break;
36 }
37 }
38
39 return 0;
40 }
(3)多个子进程等待
先创建多个子进程,让子进程做自己的事,当子进程退出时 , 父进程通过子进程的pid依次获取退出信息,回收资源。
1 #include
2 #include
3 #include
4 #include
5 #include
6
7 int main()
8 {
9 //waitpid 多进程
10 pid_t ids[10];
11 for(int i = 0; i < 10; i++){
12 pid_t id = fork();
13 if(id == 0 ){ //child
14 int count = 5;
15 while(count > 0){
16 printf("child %d do something!: %d, %d\n",i, getpid(), getppid());
17 sleep(1);
18 count--;
19 }
20 exit(i);
21 }
22
23 //father
24 ids[i] = id;
25 }
26
27 int count = 0;
28 while(count < 10){
29 int status = 0;
30 pid_t ret = waitpid(ids[count], &status, 0);
31 if(ret >= 0){
32 printf("wait child success!, %d\n", ret);
33 if(WIFEXITED(status)){ //这种方式是推荐的
34 printf("child exit code : %d\n",WEXITSTATUS(status));
35 }
36 else{
37 printf("child not exit normal!\n");
38 }
39
40 // printf("status: %d\n", status);
41 // printf("child get signal: %d\n", status&0x7F);
42 // printf("child exit code : %d\n", (status>>8)&0xFF);
43 }
44 count++;
45 }
46
47 return 0;
48 }
(1)进程程序替换时,有没有创建新的进程? 没有!
进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。
(2)子进程进行进程程序替换后,会影响父进程的代码和数据吗?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
(1) int execl(const char *path , const char *arg , ...);
第一个参数是你要执行程序的路径 ;第二个参数是你要怎么执行,以NULL结尾表示你已经把想要传的参数传完了。
示例: execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
示例: execlp("ls", "ls", "-a", "-i", "-l", NULL);
代码测试:
结果:
① 这里我们在test.c传递的环境变量通过execle函数到达mytest.c ,其实如果我们不传环境变量,OS默认会传给mytest.c系统的环境变量,我们传了,OS就不传了。
②Makefile一次生成多个可执行程序
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
char* myargv[] = { "mytest", NULL };
char* myenvp[] = { "MYVAL=100", NULL };
execve("./mytest", myargv, myenvp);
(1)exec系列函数只要返回了,就意味着调用失败。
(2)使用的execl就可以称之为:linux下的加载器所采用的的底层技术,execl本质就是把一个程序run起来
这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:
l(list):表示参数采用列表的形式列出。
v(vector):表示参数采用数组的形式。
p(path):表示能自动搜索环境变量PATH,进行程序查找。
e(env):表示可以传入自己设置的环境变量
事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。
shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可
shell执行的步骤:
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8
9 #define LEN 1024 //输入命令最大长度
10 #define NUM 32 //命令拆分后的最大个数
11
12 int main()
13 {
14 char cmd[LEN]; //存储命令
15 char* myargv[NUM]; //存储命令拆分后的结果
16 char hostname[32]; //主机名
17 char pwd[128]; //当前目录
18
19
20 while (1){
21 //获取命令提示信息
22 struct passwd* pass = getpwuid(getuid());
23 gethostname(hostname, sizeof(hostname)-1); //获取主机名
24 getcwd(pwd, sizeof(pwd)-1);//获取绝对地址
25 int len = strlen(pwd);
26 char* p = pwd + len - 1; //获取当前目录
27 while (*p != '/'){
28 p--;
29 }
30 p++;
31
32 //打印命令提示信息
33 printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
34
35 //从标准输入读取命令
36 fgets(cmd, LEN, stdin);
37 cmd[strlen(cmd) - 1] = '\0';
38
39 //拆分命令
40 myargv[0] = strtok(cmd, " ");
41 int i = 1;
42 while (myargv[i] = strtok(NULL, " ")){
43 i++;
44 }
45
46 pid_t id = fork(); //创建子进程执行命令
47 if (id == 0){
48 //child
49 execvp(myargv[0], myargv); //进行程序替换
50 exit(1);
51 }
52
53 //父进程等待获取子进程的退出信息
54 int status = 0;
55 pid_t ret = waitpid(id, &status, 0);
56 if (ret > 0){
57 printf("exit code:%d\n", WEXITSTATUS(status));
58 }
59 }
60 return 0;
61 }
运行结果:
(7)进程替换可以执行不同语言的代码
①shell脚本程序
②C++程序,经过编译形成可执行程序
③python程序