【Linux操作系统】进程控制详解,fork、进程替换、等待、实现shell解释器


秃头侠们好呀,今天来说 进程控制

进程创建

fork函数的认识

fork函数的作用是在已存在的进程中创建一个新进程,新进程为子进程,原进程为父进程。

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

1、分配新的内存块和内核数据结构给子进程
2、将父进程部分数据结构(包括:进程控制块PCB、进程地址空间、页表、构件映射关系)内容拷贝至子进程
3、添加子进程到系统进程列表当中
4、fork返回,开始调度器调度

写时拷贝

通常父子代码共享,父子在不写入时,数据也是共享的,但当任意一方需要进行写入,就会以写时拷贝的方式各自拥有一份副本数据。
当有人要写入的时候,发生缺页中断,OS把数据拷贝一份,再让映射关系修改一下,使其具有独立性,父子进程互不干扰。(详细看下图)
在这里插入图片描述

fork常规用法

①一个父进程希望复制自己,使父子进程同时执行不同的代码段。比如:父进程等待接待客户请求,子进程处理请求。
②想让子进程执行完全不同的一个程序。比如,子进程从fork返回后,调用exec进程替换。
其实就是要么子进程做父进程的一部分工作,要么子进程去做全新的工作。
(子承父业/自己创业)

fork失败的原因

创建进程是有成本的(时间、空间),如果系统中有太多进程,或者实际用户的进程数过多,都有可能造成fork失败。

进程终止

进程退出场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果错误
  • 代码异常终止(没跑完就终止了),程序崩溃(退出码没意义)

进程常见退出

正常终止(可以通过 echo $? 查看进程最近一次的退出码)。

  1. 从main返回
  2. 调用exit
  3. _exit

我们熟知的main()函数,最后我们都要return 0是为啥?
答:0是进程的退出码,表示成功退出。而!0则是不正确退出,具体为什么不正确,有多种可能,用具体一个数代表一种可能性。

strerror(i) 打印错误码对应的原因。

①main()函数return 代表进程退出,非main()函数,return叫函数返回
②exit(i)在任意地方调用,终止进程,参数为退出码
③_exit(i)强制终止进程,不要给我进行后续收尾工作了(比如刷新用户级缓冲区)

而①②本身会要求系统进行缓冲区刷新。

执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返
回值当做 exit的参数。

在这里插入图片描述
进程退出时,OS层面做了什么呢?
:系统层面少了一个进程,free PCB、free mm_struck(进程地址空间)、free 页表和映射关系,代码和数据申请的空间也要给释放掉。

进程等待

进程wait是什么?
:通俗说,fork():子进程帮助父进程完成任务,那父进程得知道你完成咋样了吧。

为什么要让父进程等待呢?

  1. 通过获取子进程的退出信息,能够得知子进程执行结果
  2. 可以保证时序问题,子进程先退出,父进程后退出
  3. 进程退出后会先进入僵尸状态,会造成内存泄漏问题,需要通过父进程wait,释放该子进程占用的资源。
  4. 进程一旦变成僵尸状态,刀枪不入,因为该进程以死,无法给它发送信号(kill -9也无能为力)。

进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

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:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
if(WIFEXITED(status))
{
    printf("exit code:%d\n",WEXITSTATUS(status));
}
else
{ 
    printf("error.get a signal\n");
}


options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。

父进程拿到什么status结果,一定和子进程如何退出强相关。
子进程如何退出就是刚才我们谈到的那三种状态(1完毕正确、2完毕错误、3异常)。
最终一一定要让父进程通过status获得子进程执行的结果。

代码异常终止的本质:是这个进程因为异常问题,导致自己收到了某种信号(正常为0)。
如果没有收到信号就是跑完了,这时再看退出码,看是正确跑完还是错误跑完。

status不能简单看作整形
(32位比特位,只用低16位)
在这里插入图片描述

int main()
{
  printf("I am child pid:%d,ppid:%d\n",getpid(),getppid());
  exit(10); 
}

在这里插入图片描述bash是命令行启动的所有进程的父进程。
bash一定是通过wait方式得到子进程的退出结果,所以我们echo $?能够查到子进程的退出码。

我们画图理解一下waitpid的status是怎么获取子进程信息的
在这里插入图片描述wait和waitpid都可以回收子进程的僵尸!

第三个参数:options

如果设为0则是默认行为:阻塞等待,父进程啥也不干,就等此进程退出。

如果设为WNOHANG,等待方式为非阻塞,需不断检测它的运行状态,但不会卡住自己,还可以做自己的事情。可能需要多次检测,基于非阻塞等待的轮询方案。

举个例子,比如你打电话让朋友下楼给你送东西,但是这时朋友正在忙个事,它让你等几分钟,如果不挂电弧等他,就是第一种,如果你挂了电话玩会游戏,停2分钟再给他打问好了没,就是第二种。

阻塞了是不是意味着父进程不被调度执行了呢?
在这里插入图片描述
阻塞的本质是:进程的PCB被放入等待队列,并将进程的状态设为S状态。
返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度。

WNOHANG:非阻塞
1、子进程根本没退出(父进程可以做自己的事,不用干等)
2、子进程退出,waitpid(调用成功/失败)

int status=0;
  while(1)
  {
    pid_t ret=waitpid(-1,&status,WNOHANG);
    if(ret==0)
    {
      //子进程没退出但是waitpid是成功的,需要父进程重复等待,可以做自己事
    }
    else if(ret>0)
    {
      //子进程退出了,waitpid也成功了,获取到了对应的结果
      printf("%d,%d,%d\n",ret,(status>>8)&0xFF,status&0x7F);
    }
    else{
      //等待失败
      perror("waitpid");
      break;
    }
  }

进程程序替换

目前,我们创建子进程是为了什么?
通过if else if让子进程执行父进程代码的一部分。
那如果想让子进程执行全新的代码呢?
使用:程序替换

进程不变,仅仅替换当前进程的代码和数据的技术叫做进程的程序替换
它没有创建任何新的进程,所以进程id不变,而是用老进程的壳子。
本质就是:把新的代码数据加载到老壳子里(特定进程的上下文)。

程序本质也是一个文件
文件=程序代码+程序数据(都在磁盘上)
在这里插入图片描述C/C++程序想要运行,必须先加载到内存中。
如何加载?加载器
而exec*程序替换函数就相当于加载器。

进程是具有独立性的!
子进程进行替换,为何父进程没有受影响?父子进程不是代码共享吗?
:进程替换会更改代码区的代码,就要发生写时拷贝了!

只要进程的程序替换成功,就不会执行后续代码,意味着exec函数成功的时候是没有返回值的,不需要返回检测。
所以只要exec
返回了,就一定是调用失败了。返回-1。

基本使用

在这里插入图片描述

#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 execve(const char *path, char *const argv[], char *const envp[]);
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

怎么在Makefile一次形成多个执行

.PHONY:all
all:myexe myload
myexe:myexe.c
	gcc -o $@ $^
myload:myload.c
	gcc -o $@ $^
.PHONY:clean
clean:
	rm -f myexe myload

所有接口没有太大差别,只有参数不同,为了适应不用应用场景。
execve是系统调用接口,其他都是库函数,就是对系统调用做了不同封装而已。

模拟实现shell命令行解释器

#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>

#define NUM 128
#define CMD_NUM 64

int main()
{
  char command[NUM];
  while(1)
  {
    char*argv[CMD_NUM]={NULL};
    command[0]=0;
    printf("[who@myhostname mydir]# ");
    fflush(stdout);
    fgets(command,NUM,stdin);
    command[strlen(command)-1]='\0';//清理一下\n
    const char*sep=" ";
    argv[0]=strtok(command,sep);
    int i=1;
    while(argv[i]=strtok(NULL,sep))
    {
      i++;
    }
    if(strcmp(argv[0],"cd")==0)//检测命令是否需要shell本身执行,内建命令
    {
      if(argv[1]!=NULL)
      {
        chdir(argv[1]);
      }
      continue;
    }
    if(fork()==0)
    {
      execvp(argv[0],argv);
      exit(1);
    }
    int status=0;
    waitpid(-1,&status,0);
    printf("exit code:%d\n",(status>>8)&0xFF);
  }
  return 0;
}


⭐感谢阅读,我们下期再见
如有错 欢迎提出一起交流

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周周汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值