一、进程创建
1、为什么会有两个返回值?2、写时拷贝 二、进程退出
1、exit vs _exit2、return 三、进程等待
1、为什么要进程等待呢?2、wait和waitpid 四、进程替换
1.OS如何重新建立映射关系?2、execl 和 execv的区别 五、shell的运行原理实现
一、进程创建前言:本节主要讲的是进程具体的调用方法及原理,如何调用对应的系统接口进行进程创建、等待和退出的方法
先看一下创建进程的系统函数
当返回 == 0,就是子进程
当返回值 > 0,就是父进程,返回的是各子进程的id
当返回 -1 , 创建失败
我们创建一个进程 pid_t id = fork();
都说子进程会拷贝父进程的地址空间代码数据、页表,那么是如果有if条件判断是如何区分跑的哪一个?
这里的fork函数,父进程创建子进程,赋值给id变量,这时候就已经发生了写时拷贝,这时子进程拷贝了父进程的内容,都相互独立,而fork会return各自的id,互不影响
就好像我们用的Terminal一样,自身是一个父进程,有把握的事情自己做,没有把握的事情容易fail的,交给子进程去做,这样就不会影响用户使用此Terminal
2、写时拷贝 我们都知道在没有修改内容之前,父子进程的页表都会指向同一块物理内存
注意写时拷贝由操作系统完成
而发生了修改之后,操作系统写时拷贝为子进程新创建一个物理内存进程存储
二、进程退出 1、exit vs _exit那么问题来了为什么要有写时拷贝?
一般的思路是,父进程创建的时候,直接把数据各自拷贝一份不就完事了嘛
如果在没有发生数据修改的时候,做了一次拷贝就浪费内存和系统资源,fork时会创建各自的数据结构,如果再进程拷贝效率会很低,所以只有在发生修改时再做拷贝
_exit(&status) 调用后就直接退出了,不会有其他动作
相反 exit(XX)调用后,会执行用户定义的清理函数,冲刷缓冲,关闭流等
#include
#include
return xxx的方式等同于执行exit(xxx),因为调用main的运行时函数会将main的返回值当做exit的参数
站在OS的角度,如何理解进程终止?
释放曾经管理所维护的数据结构对象释放代码和数据所占用的空间(这里不是数据清空而是把内存设置为无效)取消该进程曾经的连接关系*释放:不是真的把数据结构对象销毁,而是设置为不可用,然后保持起来,如果不用的对象多了就会有一个“数据结构的池子”
三、进程等待 1、为什么要进程等待呢? 回收僵尸进程,解决内存泄漏问题,如果没有父进程的等待子进程会保留部分数据结构在内存中占用资源需要获取进程的运行结束状态父进程要尽量晚于子进程退出,可以规范化进行资源回收 2、wait和waitpid
如上面所述wait 正常结束会返回child process的ID,否则失败会返回-1
这里面的stat_loc会返回子进程结束的退出状态
pid_t
waitpid(pid_t pid, int *stat_loc, int options);
这里当正常返回时waitpid返回收集到的子进程的进程ID,stat_loc和wait一样也是用于获取child process的退出状态
这里stat_loc也是status,有两种已经封装好的函数
options分为阻塞和非阻塞
如果是非阻塞WNOHANG,若pid指定的子进程没有结束,则waitpid()函数返回0,不等待。若正常退出则返回的是该子进程的ID。如果是阻塞和wait一样,正常退出返回的是child process的ID num
int main( void ){{ pid_t pid; if ( (pid=fork()) == -1 ) perror("fork"),exit(1); if ( pid == 0 ){ sleep(20); exit(10); }else { int st; int ret = wait(&st); if ( ret > 0 && ( st & 0x7F ) == 0 ){ // 正常退出 printf("child exit code:%dn", (st>>8)&0xFF); }else if( ret > 0 ) { // 异常退出 printf("sig code : %dn", st&0x7F ); } }}
child exit code:10 //等待20秒后正常退出sig code:9 //在其他终端kill掉非阻塞式:
int status = 0;pid_t ret = 0;do { ret = waitpid(-1, &status, WNOHANG); if(ret==0){printf("child process is runningn");}sleep(1); }while(ret==0);
四、进程替换创建子进程的目的有两个
执行父进程的部分代码执行其他程序的代码,这里就会进行程序替换 那么什么是程序替换呢?
上图中,原本的子进程页表映射关系和父进程是指向同一块内存,即同一块代码和数据,但是当磁盘中的可执行程序运行时,内存中重新开辟了空间给代码和数据,重新改变了页表映射关系,指向新的物理地址,这就是进程的程序替换。
OS通过代码的全部写入,中断或者特殊信号,子进程自动触发写时拷贝,重新开辟空间,开辟好后再重新把磁盘中的数据,代码重新加载到我们的内存中,那么页表就会重新建立映射关系
一个程序要运行就必须先加载到内存中,程序运行起来要先有进程,exec*可以理解成特殊的加载器
程序进行替换的时候,有没有创建新的进程?
没有,因为我们不需要创建进程PCB,地址空间,页表,并且子进程的pid也没变
下列为C语言标准库中execl函数
#include
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
如果调用成功,加载新的程序从启动代码开始执行,不再返回如果调用失败返回-1因此exec函数只有出错的返回值,而没有成功的返回值因为后续代码不会执行了
⚠️WARNING:最后都是以NULL进行结尾
2、execl 和 execv的区别如果执行失败的话,则执行第六行的报错,成功则不会
这其实是因为执行成功之后的代码已经看不见了,我们的wait/waitpid拿到的退出信息是被替换的进程的退出码,成功退出码status为0, 失败了则会返回指定的退出码,如exit(567),则status为567
如上图所示execv用vector的形式呈现
用户通过shell解释器用top、ls、pwd等相关可执行程序,这其中shell解释器不是直接通过其自身进程来完成,而是通过fork()给子进程去执行这些命令( 程序替换exec*),OS进程操作后返还给shell,shell再给用户
实现了一个基本的minishell
#include
⚠️WARNING : 上述代码有个很重的点,子进程的操作是不会影响父进程的比如 cd …/…/… ,发现pwd后路径还是原来的,因此这里我们使用了command_parse[0]为cd的时候单独判断,因为这里不单单是改变子进程了
创作不易,如果文章对你帮助的话,点赞三连哦:)