目录
前言
进程创建
fork()函数
写时拷贝
进程终止
退出场景
退出方法
进程等待
等待原因
等待方法
1.wait函数
2.waitpid函数
等待结果(status介绍)
进程替换
替换原理
替换函数
进程替换例子
shell简易实现
后记
学习完操作系统中进程部分的入门介绍之后,大家应该进程有了个初步了解,那么,下面就可以很好地进军进程控制部分了,包括进程创建、进程终止、进程等待、进程替换等重点部分,其中的细节很多,也比较难以理解,但是没有关系,在介绍完进程控制之后,会简单实现一个shell程序,也就是类似Xshell的一个软件,也可以执行相关命令进行各种操作,来综合理解一下四个重点部分,快往下看吧!
在进程入门理解章节中,我们介绍到了fork()函数,可以创建一个新进程,此进程称为子进程,原进程称为父进程,函数信息如图所示
还知道,fork失败时返回-1,成功时有两个返回值,给父进程返回子进程的pid,给子进程返回0,所以fork()之后由此分流,使得父子进程去做不同的事。
1)fork()深入理解
由于进程=内核数据结构+进程的代码和数据,其中内核数据结构由os搞定,而进程的代码和数据一般从磁盘来,也就是c/c++运行的可执行文件。所以fork()之后,os创建子进程,为其分配对应内核数据结构(必须子进程独有,因为进程具有独立性),理论上,子进程也要有自己的代码和数据,这如何拥有?
对于代码,都是不可写的,所以父子共享,对于数据,可写可读,所以不能共享,必须分离。这里先针对于代码,数据的分离会在下面的写时拷贝讲到。
见上图想一下,fork()之后,父子进程是共享after的代码还是共享所有的代码?所有的!但子进程从after那里开始执行,而不是从头开始执行。下面先提一下两个认知:
①代码汇编以后会有很多行代码,在加载到内存之后,每行代码都有自己对应的地址;
②cpu中有一个寄存器叫做EIP,也叫做pc指针、程序计数器,记录当前正在执行代码的下一行代码的地址,属于进程的上下文数据。
创建子进程时,EIP的值无需给子进程,因为父子进程各自调度,会修改EIP,就算给了子进程也用不到,在子进程中会将after的第一行代码的地址赋给EIP,进程就从after开始执行了。
值得注意的是,fork()之后,父子进程两个执行流分别各自执行,谁先执行完全是由调度器决定的。
2)fork()常规用处
①一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
②一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec系列函数进行进程替换。
3)fork()调用失败的原因
①系统中有太多的进程;
②实际用户的进程数超过了限制。
在上文讲到,对于代码,都是不可写的,所以父子共享,对于数据,可写可读,所以不能共享,必须分离。那如何分离呢?直接拷贝一份然后修改?
不行!这样的话会存在子进程不会用到的空间,造成内存浪费,即使有用到的空间,也可能只是读取,所以,数据分为不会被访问或指挥读取的数据和将来会被父或子进程写入的数据,但一般而言,os无法得知哪些数据不会被访问或者只会读取亦或会被写入,所以os选择了写时拷贝技术。
也就是说,父子进程两方都没有数据写入操作时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式重新分配内存并将原来内存上的内容拷贝到新内存上,再进行修改。
好处:
①使父子进程彻底分离,保证了进程的独立性;
②写时拷贝是一种延时申请技术,提高了整机内存的使用率。
在写c/c++程序时,我们写main函数都要返回一个int,且都返回0,这个操作到底是什么意思呢?实际上,main函数作为一个进程,在结束时是要返回一个结果给操作系统的。对于main函数,返回0代表成功返回,结果正确;而返回非0值代表结果不正确或异常,因为非0值有很多,所以错误结果或异常结果就对应很多,此时这个0或者非0值叫做进程退出码,返回给上一层评判进程的执行结果的。
但是我们写完一个main函数,也不知道结果到底正不正确,进而也不知道该返回什么啊?其实是可以的,看看下面的例子,我们可以使用if语句判断是否为期望结果决定返回值,结果不正确返回1,正确返回0,当然也可以预判出其他错误返回不同的非0值。
通过main函数可以总结出,一个进程退出有三种情况:
①正常退出,结果正确;
②正常退出,结果不正确;
③异常退出。
1)正常终止的方法
①在main函数中return;
②调用系统接口_exit函数;
③调用库函数exit函数,
注意:
①必须在main函数中返回才是终止进程,普通函数返回只是在返回调用结果;
②正常终止都会返回进程退出码给os,可以通过【echo $?】查看最近一次的进程退出码,同时可以通过函数【strerror(退出码)】查看对应的退出原因,比如
2)异常终止
①ctrl+c;
②通过信号终止。
注意:通过信号终止,在下面即将要学到的信号章节中讲解,这里重点讲上面的正常退出的方法。
exit、_exit介绍与对比:
1)_exit函数
参数status存储着进程的终止信息(包括进程退出码),父进程通过wait函数接收该值,这里在进程等待部分重点讲解。
2)exit函数
这里的参数status与_exit函数中的一样。
3)对比
①exit函数与_exit函数在代码的任何地方调用都表示结束进程,无论在main中还是调用的某个函数中。
②其实,exit库函数是_exit系统接口的一个封装,在exit函数内部,也会调用_exit函数,但在这之前,还会执行清理函数,并且清理缓冲区,然后再调用_exit函数,如图。
注意:return结束进程更为常见,return n相当于exit(n)。
在前面说过僵尸进程的问题,即子进程退出但父进程不管不顾,就会造成内存泄漏。按照正常情况,父进程创造出一个子进程肯定是要其完成一个任务,然后子进程去完成,父进程等待子进程终止以后返回的结果,这就是进程等待。通过这种方式,父进程回收子进程的资源及获取子进程退出信息(比如进程退出码)。
返回值:成功接受到被等待进程返回该进程的pid,失败返回-1;
参数:status是一个输出型参数,即传进此参数,函数内会将进程信息放进这个指针中,函数返回后,父进程可通过此值查看子进程信息,若不想得到父进程的结束信息,就传入NULL,关于此参数的构成会在下文提到。
eg:
参数:
pid:①传入指定被等待的进程pid,②传-1,代表等待任一个进程,与wait等效;
status:与wait函数一致;
options:①传WNOHANG,代表若指定进程没有结束,则函数直接返回0,不再等待,若进程已经结束,则函数直接返回子进程pid,即非阻塞等待;②传0,代表当子进程没有结束,父进程处于阻塞状态去等待其结束,与wait等效。
注意:WNOHANG是一个宏定义,一般大写的标记位都是宏定义。
返回值:
与wait一致,但要注意设置了WNOHANG选项的返回值。
eg:
对于wait/waitpid函数,都有一个输出型参数status,os将子进程信息填入其中,带给父进程。status不能简单的当作一个整形来看,要分开看它的比特位(目前只关心status的低16个比特位),如图
可以看到,低八位存放终止信号,此低八位存放退出码,对于异常终止时的core dump标志暂时不说明,后面信号章节会说到。明显地,当wait/waitpid函数接受完子进程退出结果之后,正常退出可以通过【(status>>8)&0xFF】获取退出码,异常退出可以通过【status&0x7F】获取终止信号,有点C语言地基础都可以看的懂,不多赘述。因为比较麻烦,所以Linux也提供了可以关于此的宏定义:
①WIFEXITED(status):查看进程是否正常退出,若正常退出返回真,否则返回假;
②WEXITSTATUS(status):查看进程的退出码。
eg(除了else部分,其他部分与上张截图一样):
eg(增加了子进程睡眠时间,中间通过kill指令杀掉进程):
通过特定的接口,加载磁盘上的一个全新的程序(代码和数据)到内存中,并和当前进程的页表重新建立映射,这就叫做替换,而其中加载的方法就是使用exec系列函数。当进程调用一种exec函数时,该进程的代码和数据完全被新程序替换,从新程序的开头开始执行,原理图如下。
注意:
①调用exec函数并没有创建新进程,所以调用前后的进程id并没有变化;
②当子进程加载新数据时,代码和数据就会被替换,对于代码而言这正是一种写入,即写时拷贝,此时,父子进程彻底分离,虽然曾经并不冲突(之前说过,父子进程代码共享,数据采用写时拷贝的手段)。
如图一,替换函数有6种,统称为exec函数,而图二的一个exec函数是系统调用函数,图一的6个函数都是基于这个系统调用函数封装的函数,以满足不同的需求,这里我们也是重点介绍上面6个函数。
参数:
path参数是个指针,需要传入一个路径(字符串),
arg参数也是个指针,需要传入一个指令,而后面的省略号是可变参数列表,可以传入指令的选项,
file参数:指针变量,传入一个文件名,
envp:指针数组,里面存放环境变量;
argv参数:指针数组,里面存放命令行参数,即全部arg参数。
返回值:
如果调用成功则加载新的程序从新程序的启动代码开始执行,不再返回,如果调用出错就返回-1。
注意:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
eg:
注意:可变参数列表列出所有的选项后要以NULL结尾,命令行参数数组也是如此。
1.如何替换自己的c/c++程序
自己通过vim或者其他编辑器编写一个c/c++程序,这里我编写了一个cmd.c的文件,其中main函数的参数可以传入命令行参数个数,命令行参数数组,及环境变量数组(这里我没有传入),函数体根据命令行参数传入所构成,具体如下图一所示。
之前说过,我们可以创建一个子进程,让其执行其他的事,父进程等待回收以接受结果,图二就是在这样一个情况下,我们将子进程替换成自己写的如图一所示的子程序,如图二。
2.如何替换其他语言的程序
替换其他语言的程序与c/c++语言的程序并无二质,都先编译成可执行程序,然后将子进程替换成自己写的程序就行,这里举例python程序和shell程序,具体如下图一
如图二则是替换python程序的结果,替换shell程序一样。
在学习Linux的过程中,离不开xshell的帮助,这个软件可以远程访问服务器,通过指令管理服务器上的文件等,比如,ls、pwd等。思考一下,我们可不可以通过程序控制来简易实现一个shell程序,步骤如下:
①获取命令行;
②解析命令行;
③创建子进程;
④替换子进程;
⑤父进程等待接收子进程;
⑥循环以上步骤。
代码:
#include
#include
#include
#include
#include
#include
#define CMD_LINE_SIZE 1024
#define ARGC_MAX 32
char cmd_line[CMD_LINE_SIZE];
char* _argv[ARGC_MAX];
int main()
{
//程序不退出
while(1)
{
//用户名+主机+当前目录
//[phan9@iZf8z8xmdh7b2erpqis8sxZ test_os_8_21]$
printf("[root@localhost shell]# ");
//初始化
memset(cmd_line,'\0',sizeof(cmd_line));
//获取用户输入的指令
if(fgets(cmd_line,sizeof(cmd_line),stdin)==NULL)
continue;
cmd_line[strlen(cmd_line)-1]='\0';
//将指令选项导入指令数组
int index=0;
_argv[0]=strtok(cmd_line," ");
//将指令选项导入指令数组
while(_argv[index])
{
index++;
_argv[index]=strtok(NULL," ");
}
//创建进程
pid_t id=fork();
if(id==0)
{
//子进程
execvp(_argv[0],_argv);
exit(1);
}
//父进程继续
int status=0;
int res=waitpid(-1,&status,0);//阻塞等待
if(res>0)
printf("退出码:%d\n",WEXITSTATUS(status));
}
return 0;
}
以上是基本的shell框架,可以自行加入一些功能,比如【ls -l】指令简写成【ll】指令,文件名变色,如图地方加入
本篇文章的知识点加上上篇进程入门介绍文章的知识点,大家应该对操作系统中的进程所涉及的知识点有了比较全面的认识了,相信反复阅读两篇文章,再加上自己尝试实现一个简易的shell程序,可以更加的深入认识,两篇文章有不懂的地方可以私我或者发在评论区有大伙共同解答哦,加油,拜拜!