目录
接上面内容
1.fork函数的进程创建
1.fork返回值
fork调用后得到两个进程:父进程和子进程;父进程的数据结构内容复制给子进程。
2.fork返回值
首先操作系统开发时就设置了函数的返回值,规定父进程返回子进程的pid,子进程返回0。
此后fork函数调用,在用户层面我们调用了fork函数,其实fork的调用,是让操作系统调用系统中创建子进程的函数运行,该函数负责拷贝父进程的数据结构内容和页表,然后将子进程也并入进程中,使得fork函数调用中就将子进程创建。最后返回pid也就实现了父子进程的分离。此时父子进程正式分别运行。
意味着子进程的创立,返回两个pid的值也就是说fork后,子进程复制了父进程的数据结构内容,随后进行了写时拷贝。二者共享父进程的代码,但是由于fork的反回值的不同,可能会进行执行部分代码的实现。
3.fork调用失败
进程太多。已经超过用户进程管理的限制。
2.写时拷贝
由于fork生成的子进程其实基底是复制拷贝父进程的数据结构内容,所以只要他们中的内容没有被修改,他们虚拟地址指向物理地址的单元是一样的。但是修改后,谁修改谁发生写时拷贝,也就是说修改的进程的页表映射的物理地址不再指向同一个单元,而是重新开辟一个空间,使得两个进程不互相影响。
3.退出码的知识
return 0:在操作系统看来,其实返回的值是退出码;用于标定进程执行结果是否正确。
$?:返回最近一次执行的进程返回的退出码。
所以,如果我们不关心main的返回值,我们直接返回0;如果关系进程的退出码,要返回特定的数据表明特定的错误。退出码意义:0成功;非0失败,不同的数表达不同的错误,需要被解释。
4.进程退出
1.退出的情况
进程结束无非三种情况:
1.运行结束,结果成功
2.运行结束,结果失败
3.运行异常,终止
2.正常退出
1.从main函数return返回退出
2.任意地方调用exit(code)
3._exit退出
exit与_exit的退出进程的操作基本一致。但是他们还是有区别的:
1.exit是C库中的函数调用;_exit是操作系统提供的调用。exit的底层实现其实是_exit;
2.exit终止进程,主动刷新缓冲区;_exit终止进程,不会刷新缓冲区
缓冲区在用户级的缓冲区。
5.进程等待
1.调用系统等待函数杀死僵尸进程
进程在运行完后,还不能直接结束。它进程是需要等待操作系统帮他杀掉的,因为操作系统要清楚进程的状态和执行任务的完成情况。所以进程等待的目的也是为了:1.回收子进程资源,以达到杀死进程的效果2.获取子进程的退出信息,得到任务的完成状态
wait()函数调用:
调用该函数,会让执行该进程的处在僵尸状态的子进程结束进程
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { pid_t id = fork(); if(id < 0) { printf("fork error\n"); return 1; } else if(id == 0) { int cnt = 10; while(cnt) { printf("我是子进程,pid:%d,ppid:%d,cnt: %d\n",getpid(),getppid(),cnt--); sleep(1); } exit(0); } else { sleep(15); pid_t ret = wait(NULL); if(ret>0) { printf("wait suxxess:%d\n",ret); } } sleep(5); return 0; }
waitpid()函数调用:
输入进程pid,进程接受的退出码,状态
调用该函数,会让执行该进程的处在僵尸状态的子进程结束进程,同时status返回退出码。
此外status有自己的位图结构。status只看前面十六位确定状态。
异常的信号可通过kill -l可查询
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { pid_t id = fork(); if(id < 0) { printf("fork error\n"); return 1; } else if(id == 0) { int cnt = 5; while(cnt) { printf("我是子进程,pid:%d,ppid:%d,cnt: %d\n",getpid(),getppid(),cnt--); sleep(1); } exit(10); } else { int status pid_t ret = waitpid(id,&status,0); if(ret>0) { printf("wait suxxess:%d,sig number: %d,child exit code: %d\n",ret,(status & 0x7F),(status>>8)&0xFF); } } sleep(5); return 0; }
如果运行异常,sig number输出异常信号,child exit code输出0(0是没有意义的意思)
2.僵尸状态与PCB的关系
僵尸状态下,进程几乎没有内存占着,它执行结束已经可以把创造的变量释放。但是唯独它的状态(退出信号)和任务完成与否信息(退出码)需要保存,而这些保存在PCB中,也就是说PCB在僵尸状态下不能被释放。而进程等待后时要把僵尸进程的PCB里的退出码和退出信号传给父进程中。
WIFEXITED(status):子进程正常终止,则返回真
WEXITSTATUS(status):若子进程正常终止,返回退出码
3.进程阻塞等待与非阻塞等待方式
阻塞等待:父进程等待子进程变为僵尸进程后,才将子进程PCB中的退出码等拿到,并且进行子进程的退出,在进行父进程的执行
非阻塞等待:确认子进程的状态,父进程不会等待子进程,如果子进程僵尸就释放子进程,没有,父进程依旧退出执行自己接下来的代码。
轮询检测:不断的进行非阻塞等待,确认子进程状态。
waitpid的第三个参数为WNOHANG便是非阻塞。
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> int main() { pid_t id = fork(); if(id < 0) { printf("fork error\n"); return 1; } else if(id == 0) { int cnt = 5; while(cnt) { printf("我是子进程,pid:%d,ppid:%d,cnt: %d\n",getpid(),getppid(),cnt--); sleep(1); } exit(10); } int status = 0; while(1) { pid_t ret = waitpid(id,&status,WNOHANG); if(ret==0) { //函数调用成功,子进程没有退出 printf("wait done,but child is running"); } else if(ret>0) { //函数调用成功,子进程退出 printf("wait success\n"); printf("wait suxxess:%d,sig number: %d,child exit code: %d\n",ret,(status & 0x7F),(status>>8)&0xFF); break; } else { //函数调用失败 printf("waitpid code file\n"); break; } } sleep(5); return 0; }
使用非阻塞等待的好处:非阻塞等待时,父进程不会被占用所有的时间用于等待子进程,它可以处理其他的任务。
6.进程程序替换
1.替换程序的可行性
以上我们讨论的子进程都是用来处理父进程复制的代码。也就是说,该种子进程的功能是为了让子进程执行父进程的一部分。那么我们也可以用子进程执行另外一个程序。
2.exec*函数
execl:找指定的程序路径,将程序加载到内存中,并且执行
path:找到对应的进程路径;
arg:执行什么操作;
... :可变参数列表,不写要以NULL结尾;
通过调用该函数,进程可以运行另外的程序;如果出错,返回-1。
execlp:找指定的程序名加载到内存中,并且执行
file:在环境变量里找需要执行的程序名,系统会自动找;
execlv:将程序的所有的执行参数一起执行
arg[]:指令不需要分开写;
execle:传入自定义环境变量
envp[]:里面是自定义的环境变量,用于替换的程序之中;
如果输出错误,直接执行原本程序的下面代码
如果是操作系统的指令,只需符合上面的操作即可;
如果是自己写的可执行文件,就不能用execlp了因为没在环境变量里,使用execl用路径查找;什么语言都可以调用。
演示:
3.exec函数执行原理
调用时,其实内存中就是对进程的数据和代码进行管理。那么execl函数便是使磁盘中对应的文件的数据和代码覆盖到内存中,必要时改变一下页表。以达到程序的替换。进程替换没有创建新的进程,只是把当前的进程内容取而代之,进行执行。
如果是父子进程的形式,子进程执行其他程序,那么子进程不是复制一份数据结构内容这么简单,它会使得整个映射失效,重新建立属于程序的映射。
4.模拟shell应用
#include<stdio.h> #include<unistd.h> #include<assert.h> #include<stdlib.h> #include<sys/types.h> #include<sys/wait.h> #include<stdlib.h> #include<string.h> #define NUM 1024 #define OPT_NUM 64 char lineCommand[NUM]; char* myargv[OPT_NUM]; int main() { while (1) { //打印输出提示符 printf("用户名@主机名 当前路径# "); fflush(stdout); //获取用户输入, char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin); assert(s != NULL); (void)s; //清楚最后一个/n; lineCommand[strlen(lineCommand) - 1] = 0; //切割字符串 myargv[0] = strtok(lineCommand, " "); int i = 1; while (myargv[i++] = strtok(NULL, " ")); #ifdef DEBUG for (int j = 0; myargv[j]; j++) { printf("myargv[%d]:%s\n", j, myargv[j]); } #endif pid_t id = fork(); assert(id != -1); if (id == 0) { execvp(myargv[0], myargv); exit(1); } waitpid(id, NULL, 0); } }