【Linux】进程的创建,中止,等待和替换

本文深入探讨Linux进程管理,包括fork函数的原理与应用,进程的创建、中止及等待机制,以及进程替换的exec系列函数。通过实例解析,展示了如何创建子进程、如何处理进程退出状态以及如何执行新的程序。最后,通过制作简易shell的步骤,阐述了进程管理在实际操作中的应用。
摘要由CSDN通过智能技术生成

一、进程的创建

i. 认识fork函数

 linux中的fork函数可以从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。
子进程执行该函数返回值为0,父进程执行该函数返回值为子进程的pid,如果出错则fork函数返回-1

ii. 关于fork函数的思考

为何给子进程返回0,给父进程返回子进程的pid?
 父进程是唯一的,因而无需标识,而子进程可以有多个,所以需标识(pid来标识),此外子进程是要执行任务的,所以需要标识来区分和指定

为何frok()会有两个返回值?
 在fork函数内部会完成子进程的数据结构创建并加入到系统的进程列表,即已经创建完了子进程,当fork函数内部子进程创建工作完成后,进程可以分别调度了子进程和父进程,它们分别执行return语句,所以有两个返回值

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

  1. 分配新的内存块和内核数据结构(task_struct和mm_struct)给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度

iii. 关于写时拷贝

上文说到当fork函数创建子进程的时候会将将父进程数据和代码拷贝至子进程,通常而言,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,(数据)便以写时拷贝的方式各自一份副本

  1. 对于共享的理解:它们通过页表指向的物理内存是一致的
  2. 写时拷贝图解
    在这里插入图片描述
  3. 为何要写时拷贝
    上面说到父子进程代码数据共享,当某个进程数据要进行写入的时候,直接写入则会影响另一个进程,但是保证进程的独立性,互不影响,故而采用写时拷贝的方式,即当任意一方试图写入,开辟一个新空间新建一份副本并修改页表的映射关系
  4. 上述说的写时拷贝指的是数据,代码会写时拷贝吗?
    绝大多数情况下不会,但在进程替换的时候会(下文讲述)
  5. 为何不在创建子进程时就指向的物理内存分开
    a) 因为子进程不一定会用到父进程的所有数据,当要写入的时候再重新开物理内存拷贝一份做到了按需分配,不浪费空间
    b) 同时做到了延时分配:即刚创建子进程时它并不是要被立即调度的,等需要被调度的时候再分配保证了系统任何时候的内存状态是最佳的,好比你放在银行未需要用的钱,银行可以帮你做更好的投资

iv. fork能干什么

  1. 希望生成一个子进程使父子进程同时执行不同的代码段(仍然是同一个程序)。例如,父进程等待客户端请求,生成子进程来处理请求
  2. 希望生成一个子进程执行一个不同的程序。例如子进程从fork返回后,调用exec函数

v. fork调用失败的原因

  1. 系统中有太多的进程,系统资源(内存)不够了
  2. 实际用户的进程数超过了限制

二、进程中止

i. 关于退出码

main返回值给了谁
 给了操作系统

为什么main函数要有返回值(即进程退出码)
 当程序跑起来变成了进程,用户需要知道运行结果怎么样了,则通过返回值来获得运行信息

如何获得最近一次进程退出时的退出码?
 可通过echo $?来显示最近一次进程退出时的退出码,0表示程序运行结束并成功,非0表示出现了错误,通过strerror()函数可一将不同返回值的信息打印出来

通过strerror函数将不同退出码的信息打印出来

#include<stdio.h>
#include<string.h>

int main()
{
	for(int i=0; i<100; i++)
		printf("%d:%s\n",i,strerror(i));
		
	return 0;
}

在这里插入图片描述

  • 程序运行完echo $?查看退出码信息验证与bash的报错
    在这里插入图片描述

ii. 进程退出的两种情形

1.正常退出

  • 从main返回:main函数return相当于进程结束
  • 调用exit函数
  • 调用_exit函数

对比exit和_exit的区别:
exit会释放进程曾经占用的资源,比如缓冲区,而_exit直接中止进程,不做任何收尾工作,在任何地方使用exit函数都可以使进程退出,exit退出时会帮我们刷新缓冲区,如里面有字符串会被打印出来
在这里插入图片描述
可以把exit函数看作_exit函数的完善版,事实上exit函数在做完清理工作会调用_exit函数

2.异常退出

  • 进程收到某个信号,使之终止:当异常退出了,退出码是随机值没有意义。进程中止操作系统会释放曾经申请的内存,释放task_struct,mm_struct,去除页表中的对应关系

三、进程等待

i. 为什么要有进程等待

当子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏,所以要回收子进程资源,同时获取子进程退出信息,故而有了进程等待

ii. 进程退出的结果status

进程等待通常通过wait函数waitpid函数来完成,他们都有个输出型参数status,用于接收进程退出的结果,当你向函数传递status的地址,操作系统会自动填充进程退出的结果到status,如果不关心子进程的退出结果则传递NULL,下面详细分析下status是什么:

  • status是个整形数,可以当作位图来看待,我们只研究status低16比特位,当进程正常中止或被信号所杀时status低16比特位存放的信息如下图
    在这里插入图片描述

  • 正常中止:bit[8-15]表示退出状态即退出码,也就是说将status(退出结果)的高8位剥离下来才是退出状态,因此通过(status>>8)&0xFF才得到退出状态

    代码展示如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
	pid_t id = fork();
	if(id == 0)//child process
	{	
		printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
		sleep(1);
		exit(2);//将退出码设置为2
	}
	//father process
	int status = 0;
	pid_t ret = wait(&status);
	if(ret > 0)//wait success
	{	
		printf("wait child success,pid:%d\n",ret);
        printf("status is:%d\n",status);
        printf("child exit code:%d\n",(status>>8)&0xFF);
	}
	return 0;
}

  结果如下:
在这里插入图片描述
  我们可以发现打印出来的status为512,并不是我们设置的退出码2,通过status>>8)&0xFF则获取到了退出码

  • 被信号所杀:bit[0-6]表示退出状态即退出码,也就是说将status(退出结果)的低7位剥离下来才是中止信号,因此通过status&0x7F才得到退出状态
    上述代码加上printf(“sig code : %d\n”,status&0x7F );,运行起来后在终端输入kill -9则可杀死该进程,终端会打印出sig code:9

iii. 进程等待的方法

wait方法
方法原型:pid_t wait(int*status);
关于返回值:成功返回被等待进程pid,失败返回-1
关于参数:status在上文已阐述
总结:通过上述代码知道了父进程有wait后子进程退出并不会变成僵尸

  1. 子进程退出了信息没被回收则会变成僵尸状态,若父进程有wait()函数子进程就不会进入僵尸状态
  2. 当子进程运行期间,父进程wait时,父进程在等子进程退出,不做其他工作,这个叫做阻塞等待
  3. 虽然父子进程谁先运行不确定,但是有了wait()函数之后大部分情况都是子进程先退出,父进程读取子进程信息再退出

waitpid方法
方法原型:pid_ t waitpid(pid_t pid, int *status, int options);
关于返回值

  1. 当正常返回的时候waitpid返回收集到的子进程的进程ID
  2. 如果参数options设置为WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

关于参数

  1. pid
    可指定pid来wait特定的子进程
    pid>0:即指定pid那么只会等待该进程
    pid=-1:等待任一个子进程,与wait等效
  2. status
    要获取status相关信息不止可以通过移位操作,还可以用下面两组宏
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,直接返回子进程退出码。(查看进程的退出码)
  3. options
    通过设置为WNOHANG或0分别进入非阻塞等待状态或阻塞等待状态,下文【iv.】再分析

总结:父进程等待成功代表子进程运行成功吗?
 不是,因为进程等待成功只说明了子进程退出了,是否运行成功还得看退出结果分析,子进程退出有两种:

  1. 正常中止,关心退出状态(退出码)
  2. 被信号所杀,异常中止,关心终止信号

iv. 关于阻塞和非阻塞等待

  • 无论是wait还是waitpid都是阻塞等待

  • 讨论waitpid的option参数:
    1.当设置为WNOHANG:进入非阻塞状态,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
    2.当设置为0:进入阻塞等待状态

  • 基于非阻塞接口的轮询检测方案代码实现

//关于轮询非阻塞等待的实现
     pid_t id = fork();
     if(id==0)
     {
         //child
         int count=0;
         while(count<10)
             printf("I am child, pid:%d,ppid:%d\n",getpid(),getppid());
             sleep(1);
             count++;
         }
         exit(1);
     }
     //父进程轮询等待
     while(1)
     {
         int status=0;
         pid_t ret=waitpid(id,&status,WNOHANG);
         if(ret>0)//等待成功
         {
             printf("wait success!\n");                                          
             printf("exit code:%d\n",WEXITSTATUS(status));
             break;
         }
         else if(ret==0)//没等到
         {
         //此处可以写父进程要完成的代码,即等不到子进程父进程可以干自己的事
               printf("father do its own things!\n");
              sleep(1);
         }
          else//等待错误
          {
             printf("error\n");
              break;
          }
     }

四、进程替换

i. 替换原理

 用fork创建子进程,子进程执行的是和父进程相同的代码不同的分支,而要完成进程替换让子进程执行其他代码需要调用exec函数。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变

ii. 替换类函数exec

  1. 认识六组exec函数:
#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[]);

参数const char *path是要执行程序的路径;参数const char *arg是可变参数列表,表示你要如何执行这个程序,并以NULL结尾;参数const char *file是要执行程序的名字;参数char *const envp是自定义环境变量名称;参数char *const argv是一个指针数组,数组当中的内容表示你要如何执行这个程序,以NULL结尾

观察六组exec函数,发现它们都是由exec衍生而来,再加上 l、v、p、e 等,下面阐述它们代表的意思

  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示使用自己维护的环境变量,需自行添加环境变量
  • 无p/e:需指定路径

 注:l 和 v 这两个参数是对立的,二选一,是传参的两种形式;p 和 e这两个参数是互斥的,环境变量的两种形式,可以不加p/e,指定执行路径即可

  • exec函数使用分析
    对exec类函数的使用主要关注于参数路径的传递
    参数传递有两种:1. 逐个传参以NULL结束 2. 传递参数数组,数组中以NULL结尾
    路径的传递有三种:1. 指定绝对路径 2.使用系统环境变量PATH 3.使用自定义环境变量
    在这里插入图片描述
     调用exec类函数可替换系统的命令(如:ls,pwd,ps…)也可替换自己写的程序

      - 当前进程进行程序替换的时候没有创建新的进程
      - 替换成功则后续代码不再执行,不再返回,所以无需关注返回值
      - 替换不成功直接继续执行后续代码,当无事发生,程序后续不受影响
    
  1. 对比execl和execlp函数
    在这里插入图片描述

  2. 对比execl和execv函数
    在这里插入图片描述

五、制作一个简易shell

i. shell的流程

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出,获取退出结果(wait/waitpid)

ii. shell的代码演示

  #include<stdio.h>    
  #include<string.h>                                                                  
  #include<stdlib.h>    
  #include<sys/wait.h>    
  #include<sys/types.h>    
  #include<unistd.h>    
      
  #define Length 1024    
  #define NUM  32    
      
  int main()    
  {    
      char cmd[Length];    
      char *myarg[NUM];    
      char name[32];    
      while(1)    
      {    
          gethostname(name,sizeof(name)-1);//得到主机名    
          printf("[yxy@%s myshell]# ",name);    
          //1.获取命令行    
          fgets(cmd,Length,stdin);//从输入流获取命令    
          //2.解析命令    
          cmd[strlen(cmd)-1]='\0';    
          myarg[0]=strtok(cmd," ");    
          int i=1;
          while(myarg[i]=strtok(NULL," "))
          {
              i++;
          }
          //3.创建子进程执行命令
          pid_t id=fork();
          if(id==0)
          {
              //4.替换子进程
              execvp(myarg[0],myarg);
              exit(2);
          }
          //5.父进程检查子进程结果
          int status=0;
          pid_t ret = waitpid(id,&status,0);
          if(ret>0)
          {
              printf("exit code:%d\n",WEXITSTATUS(status));
          }
      }
      return 0;
  }
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值