• Linux-5-进程控制


    前言

    Vue框架:Vue驾校-从项目学Vue-1
    算法系列博客友链:神机百炼

    进程调度队列runqueue:

    优先级数组:

    • 调度队列不只一个队列,根据优先级划分,共有40+100个队列:
      优先级数组

      当一个优先级所拉链出来的双链表内进程都调度过后,开始遍历下一优先级所拉链出来的双链表

    位图:

    • 位图:

      一共有40+100个优先级,每个优先级上有待调度的进程则用1记录,没有待调度的进程则用0记录

      由于优先级数组queue[]一共140个,所以至少需要140bit才能记录完全每个优先级上是否有需要执行的进程

      开一个32*5大小的变量bit[5],其中第i位上0上1表示queue[i]上是否含有待执行进程

    nr_active:

    • 含义:

      当前queue[140]下共有多少运行状态下的进程

    活动队列:

    • 含义:

      当前时间片未耗尽,正在等待调度的进程们构成的调度队列

      这些在等待调度的进程们也是由优先级数组queue[]通过拉链双链表来组织的

    过期队列:

    • 含义:

      当前时间片已经耗尽,暂时不会再被调度的进程们构成的调度队列

      这些暂不会被调度的进程们也是由优先级数组queue[]通过拉链双链表来组织的

    active指针& expired指针:

    • 含义:

      active指向活动队列,当活动队列中所有进程时间片耗尽后active指向原本expried指向的过期队列

      expired指向过期队列,当活动队列中所有进程时间片耗尽后expired指向原本active指向的活动队列

    进程调度算法时间复杂度:

    • O(1):

      根据位图中为1的位,

      查找待角度进程的queue[i],

      之后遍历queue[i]拉出的PCB双链表,

      执行对应进程即可

    内存中的进程调度结构体:

    • 图示:
      进程调度机制的结构体

    进程创建:

    • 在初识进程中初步使用和了解了fork()函数,经过对进程地址空间和进程状态的学习,我们重新审视该函数:

    fork():

    • 为什么有两个返回值?

      1. 父进程返回所创建的子进程的pid
      2. 子进程返回0
      3. 创建失败返回-1

      fork()双返回值

    • 实例:

      1. 代码:

        int main(){
           	pid_t pid;
         	printf("Before: pid is %d\n", getpid());
        	if ( (pid=fork()) == -1 )
                perror("fork()"),exit(1);		//perror()为手动报错
         	printf("After:pid is %d, fork return %d\n", getpid(), pid);
         	sleep(1);
         	return 0;
        } 
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
      2. 输出:

        [root@localhost linux]# ./a.out
        Before: pid is 43676
        After:pid is 43676, fork return 43677
        After:pid is 43677, fork return 0
        
        • 1
        • 2
        • 3
        • 4
      3. 解释:

        1. fork()之前的代码只有父进程执行
        2. fork()之后的代码父子进程都执行,谁先取决于进程调度器

    mm_struct:

    • 虚拟地址也称为线性地址,从0x 0000 0000到0x ffff ffff

      其内部区域的划分其实也是通过结构体实现的:
      mm_struct & 分段的exe程序

    • 虚拟地址空间划分结构体:mm_struct{}

      struct mm_struct{
      	unsigned int code_start;			//正文代码区
          unsigned int code_end;
          unsigned int readonly_start;		//字符串常量区
          unsigned int readonly_end;
          unsigned int init_start;			//初始化数据区
          unsigned int init_end;
          unsigned int uninit_start;			//未初始化数据区
          unsigned int uninit_end;
          unsigned int heap_start;			//堆区:向下生长end++
          unsigned int heap_end;
          unsigned int stack_start;			//栈区,向上生长start--
          unsigned int stack_end;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    • 可执行程序:

      1. 程序文件通过编译器被编译为可执行文件时,已经划分好了这6大区域
      2. 可执行文件从硬盘转移到内存中时,6大区域直接照搬即可
      3. exe文件的具体格式称作ELF
    • 页表:

      数据最终是存在内存上的物理地址的,而每个进程对于虚拟内存的使用情况不同,所以每个进程的物理地址和虚拟地址的映射关系不同

      也就是说每个进程的页表不同,需要和mm_struct配套单独创建

    写时拷贝:

    • 前一篇中我们讲了父子进程原本共享相同数据和程序

      当子进程想要修改数据时,为了保证父进程的独立性,要为子进程单独新开一片存储修改了的数据的内存

      再把新开内存的物理地址和虚拟地址建立新的映射关系,加载到页表中

      这个过程就叫做写时拷贝

    • 图解写时拷贝:
      写时拷贝原理图

    页表+虚拟地址的作用:

    1. 防止直接接触OS,保护内存

      1. 一方面:进程只能访问页表内存在映射的物理地址,绝对不可能越界访问
      2. 另一方面:就算进程尝试越界访问野指针时,页表发现本进程不可访问该地址,在进程对该地址操作前就终止了异常进程
    2. 统一化内存管理

      1. 每个进程可操作的内存空间统一都是0x0000 0000 ~ 0xffff ffff

        对每个进程的内存分配处理都大体相同

    3. 维护进程独立性

      1. 每个进程在运行时,都认为自己占据着所有的资源

      2. 实现进程调度和内存管理解耦:

        程序分段加载到内存的物理地址中,这个过程是独立的

        页表将物理地址和虚拟地址建立映射,这个过程也是独立的

        进程访问虚拟地址,这个过程也是独立的

    进程创建时创建的内容:

    • 程序+数据从硬盘转移到内存
    • PCB(task_struct)块创建到内存中
    • mm_struct创建到内存中
    • 页表
    • PCB加入双链表(可能也加入了调度队列)

    进程终止:

    • 进程退出的三种情况:

      1. 代码运行正常,结果正确
      2. 代码运行正常,结果错误
      3. 代码运行异常
    • 前文我们讲僵尸进程时已经讲过程序调用关系:

      OS调用加载器,加载器调用mainCRTStartup(),mainCRTStartup()调用main()函数

      最终main()的return返回给了OS的进程退出码$

      echo $?							//打印最近一次进程退出时的进程退出码
      
      • 1
    • 进程

    main()函数的return():

    • 只有main()函数自身的return,才能将值赋予OS的进程退出码

      1. 代码:
        main()的return

      2. 输出:
        return$的输出

    exit():

    • exit(n):

      随处执行随处退出进程,且进程退出码为n

      退出后会执行后续工作:关闭输入输出流/刷新缓冲区/执行可能有的clean操作

    • 举例:

      1. 代码:
        exit()函数

      2. 输出:

        exit()输出

    _exit():

    • _exit(n):随处执行随处退出进程,且进程退出码为n

    • 与exit()区别:

      不会执行后续工作:刷新缓冲区/关闭输入输出流/执行可能有的clean操作

    • 区别图解:
      exit()和_exit()区别

    进程退出内存过程:

    • 删除内存中的附属信息:
      1. PCB结构体块:task_struct{}
      2. 进程虚拟地址空间布局:mm_struct{}
      3. 页表
    • 删除双链表中的PCB节点:
      1. PCB双链表的节点
      2. runqueue[]中queue[]中双链表的节点

    进程异常情况集strerror():

    • 进程的异常情况一共有150种,都存储在了strerroe()函数中:

      1. 代码:

        strerror(i)错误集

      2. 输出: strerror()错误集

    进程等待:

    含义:

    • 父子进程谁先运行?

      运行顺序取决于进程调度算法

    • 父子进程谁先结束?

      一方面,为了防止“孤儿进程”,一般都是子进程先结束

      另一方面,僵尸进程只能通过父进程/OS领养,回收其数据后将进程退出,无法kill -9

      这就意味着就算父进程已经执行完所有任务,最终也需要等待子进程退出后回收其数据

    • 进程等待:

      子进程运行时,父进程单纯在等,等待回收子进程资源&获取子进程退出信息

    • 父进程等待成功是否意味着子进程执行成功?

      不是,

      1. 子进程可能执行异常,结果返回异常信息
      2. 子进程可能执行顺利,返回正确结果
      3. 子进程可能执行顺利,返回错误结果

      但凡子进程执行完毕,不论是否结束,父进程都要等待回收子进程资源&获取子进程退出信息

    wait() & waitpid():

    • wait():在众多子进程中随机选择一个,返回其退出情况

      #include
      #include
      pid_t wait(int*status);				//输出型参数:status
      
      /*返回值:
       成功返回被等待进程pid,失败返回-1。
        参数:
       输出型参数,获取子进程退出状态;若不关心子进程退出情况则设置为NULL即可
      */
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • waitpid():指定一个子进程Pid,返回其退出情况

      pid_ t waitpid(pid_t pid, int *status, int options);		//输出型参数:status
      /*
      返回值:
       1,指定子进程运行完毕:返回子进程pid
       2,指定的子进程不存在:返回0
       3,调用中出错:返回-1,errno会被设置成相应的值以指示错误所在
       
      参数:
       1,pid:指定子进程pid
       	Pid=-1,等待任一个子进程。此时waitpid()与wait()等效。
       	Pid>0.等待其进程ID与pid相等的子进程。
       2,status:进程退出结果 != 进程退出码
       	WIFEXITED(status): 进程正常退出则返回1,进程异常则返回0
       	WEXITSTATUS(status): 返回进程退出码(退出码只对正常退出的进程有用)
       3,options:决定是否等待结果
       	WNOHANG: 
       		若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
       		若正常结束,则返回该子进程的ID。
      */
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
    • 使用实例:

      1. 代码:

        waitpid

      2. 输出:status不是0~149之间的错误码,而是2816,说明status和退出码$有区别

        子进程退出status

    进程退出结果status与进程退出码$:

    • 进程退出结果status本质是一个位图:

      8位退出码 + 1位core dump + 7位终止信号:

      status构成

    • 查看终止信号:

      进程运行时发生异常,导致进程收到了终止信号

      status & 0x7f
      
      • 1
    • 查看进程退出码$:

      只有status最低7位是0时,说明进程是正常退出的,此时$才有参考意义

      (status >> 8) & 0xff
      
      • 1
    • 查看正常退出进程的stutas $ 信号:

      1. 代码:kill -l展示所有信号
        正常退出的等待

      2. 输出:

        正常退出的等待的status

    • 向进程发送信号,查看其status $ 信号:
      kill -n pid

    • 查看异常运行的进程的status $ 信号:

      1. 代码:除以0

        异常status

      2. 输出:
        kill的status

    • 查看存在野指针的异常进程的status $ 信号:

      1. 代码:
        野指针进程

      2. 输出:
        野指针错误

    批量创建并查看子进程:

    • 代码:用数组保存子进程号

      int main(){
          pid_t idx[10];					//创建子进程
          for(int i=0; i>10; i++){
              pid_t id = fork();
              if(id == 0){
                  for(int i=0; i<10; i++)
      				printf("子进程 %d %d\n", getid(), getppid()):
                 	exit(1);				//子进程结束
              }
      		idx[i] = id;				//只有父进程执行
          }
          int status = 0;
          for(int i=0; i<10; i++){
      		pid_t res = waitpid(idx[i], &status, 0);
              if(ret >= 0){
      			printf("子进程%d 等待结束\n", ret);
                  printf("子进程退出状态:%d\n", status);
                  printf("子进程退出码$:%d \n", (status>>8)&0xFF);
                  printf("子进程信号:%d \n", status&0x7f);
              }
          }
      	return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

    宏查看$和信号:

    • 上述过程使用wait() / waitpid()接收status后,还需要手动移位和与

      但是其实可以使用官方给定的宏来解析获取到的status内的信息

    WIFEXITED:

    • 作用:查看所等待的子进程是否正常退出

    • 使用方式:搭配wait()/waitpid()获得status

      int status;
      pid_t ret = waitpid(dix[0], &status, 0);
      if(WIFEXITED(status)){
      	printf("child exit normally\n"):   
      }else{
      	printf("child exit error\n"); 
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    WEXITSTATUS:

    • 前提:WIFEXITED返回值为真(进程无异常)

    • 作用:查看所等待的子进程的退出码

    • 使用方式:搭配wait()/waitpid()获得status

      int status;
      pid_t ret = waitpid(dix[0], &status, 0);
      if(WIFEXITED(status)){
      	printf("child exit code:%d\n",WEXITSTATUS(status)):   
      }else{
      	printf("child exit error\n");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    进程阻塞 & 进程非阻塞:

    • 进程阻塞:

      父进程在等待回收子进程僵尸状态时的资源和数据时,什么操作也不执行,一直关注子进程是否终止

    • 进程非阻塞:

      父进程在等待回收子进程僵尸状态时的资源和数据时,运行自己的其他程序

      每过一定时间,去查询一下子进程是否运行结束

    • 阻塞/非阻塞等待模式的代码写法:

    //进程阻塞:
    waitpid(id, &status, 0);
    
    //进程非阻塞:
    waitpid(id, &status, WNOHANG);
    //W含义wait,NO含义没有,HANG含义阻塞
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 进程非阻塞模式:

      1. 代码:

        #include 
        #include 
        #include 
        #include 
        int main(){
            pid_t id =fork();
            if(id ==0){
        		for(int i=0; i<20; i++){
                    printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
                    sleep(3);
                }
                exit(1);
            }
            while(1){
        		int status = 0;
                pid_t ret = waitpid(id, &status, WNOHANG);//WNOHANG为非阻塞,HANG表示阻塞
                if(ret > 0){	//子进程回收,父进程结束等待
        			printf("wait success!\n");
                    printf("exit code: %d\n", WEXITSTATUS(status));
                    break;
                }else if(ret == 0){	//子进程未终止,父进程继续等待
        			printf("father do other things!\n");
                }else{			//子进程异常
                    printf("waitpid error!\n");
        			break;
                }
            }
        	return 0;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
      2. 输出:
        非阻塞等待

    进程程序替换:

    • 背景:

      子进程的程序和数据默认直接利用父进程

      偶然的局部数据改动通过写时拷贝来区别于父进程

      进程的程序替换就是要将子进程的所有程序和数据都从硬盘新导入,和父进程程序与数据根本没有联系

    • 进程不变:

      程序替换的时候没有创建子进程

      PCB mm_struct 页表 都没有新建

      只是PCB中对程序和数据的指针指向发生改变

    • 程序替换:由于替换的都是0101的可执行文件,所以不同语言之间都可以执行进程替换

    进程程序替换函数:

    • 程序加载器:

      1. 作用:将硬盘中的文件加载到内存中执行
      2. exec系列函数底层其实就是程序加载器
    • 六大替换函数:替换失败统一返回-1

      #include `
      //path为硬盘上的可执行程序路径,可以提前使用which查询
      //arg为参数
      //...意为可变参数源,意思是想传几个参数就传几个参数,但是必须以手写NULL结尾
      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[]);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 父进程使用举例:

      1. 代码:
        execl()

      2. 输出:程序替换execl()之前的程序还被执行,之后的程序已经被替换覆盖
        execl()输出

      3. 异常:程序替换一旦失败,execl()后续的程序不会被覆盖,还会继续执行

    • 程序调用函数的特点:

      1. 不需要返回值:一经调用,马上覆盖原程序,不需要return给原程序任何值
      2. 有返回值说明替换失败
      3. 一般搭配exit()使用,替换成功去执行新程序,替换失败原程序直接终止
    • 子进程使用举例:

      1. 代码:
        子进程execl()

      2. 输出:
        子进程execl()输出

    execl() & execv:

    • 异同:

      1. 同:都是依据路径寻找到目标文件,再进行程序替换

      2. 异:

        1. l表示参数以列表形式传入:

          execl("usr/bin/ls", "ls","-a","-i","-l",NULL);
          
          • 1
        2. v表示参数以数组形式传入

          char* argv[] = {
              "ls",
              "-a",
              "-i",
              "-l",
              NULL
          }
          execl("usr/bin/ls", argv);
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8

    execlp() & execvp():

    • 异同:

      1. 同:默认依据环境变量PATH找到目标文件,不用带路径但需要声明指令

      2. 异:

        1. lp需要声明指令+列表携带参数

          execlp("ls", "ls", "-a", "-i", "-l",NULL);
          
          • 1
        2. vp需要声明指令+数组携带参数

          char* argv[] = {
              "ls",
              "-a",
              "-i",
              "-l",
              NULL
          }
          execvp("ls", argv);
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8

    execle() & execve():

    • 异同:

      1. 同:都是依据指定路径寻找可执行文件,再通过调用程序传递自定义的“本地变量”

      2. 异:

        1. le以列表携带参数:

          char *env[] = {
              "MYENV=youcanseeme",
              NULL
          };
          execle("./cmd","cmd",NULL,env);				//./表示当前路径
          
          • 1
          • 2
          • 3
          • 4
          • 5
        2. ve以数组携带参数:

          char *argv[] = {
          	"cmd",
              NULL
          }
          char *env[] = {
              "MYENV=youcanseeme",
              NULL
          };
          execle("./cmd",argv,env);
          
          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
    • 获取OS自带的 用户自定义的环境变量:

      1. 函数:

        getenv(PATH);						//获取OS自带的环境变量
        
        getenv(自定义的环境变量名);						//获取用户传递来的环境变量
        
        • 1
        • 2
        • 3
      2. 代码:
        getenv()

      3. 输出:

        1. 未定义MYENV时:getenv(PATH)有效,getenv(MYENV)无效
          getenv(非自定义全局变量)

        2. 定义了MYENV时:getenv(MYENV)有效,getenv(PATH)无效
          getenv(自定义的全局变量)

    makefile一次产生多个可执行文件:

    • 错误写法:孤立依赖关系
      错误多文件make

    • 正确写法:伪目标综合依赖关系
      多文件makefile

  • 相关阅读:
    Solaris Exchange:一个安全可靠的合成资产交易平台
    CF 1895A 学习笔记 分类讨论
    2023华为杯数学建模D题第三问-碳排放路径优化(能源消费结构调整的多目标优化模型构建详细过程+模型假设(可复制))
    IOTDB的TsFile底层设计
    【轨道机器人】成功驱动伺服电机(学生电源、DCH调试软件、DH系列伺服驱动器)
    将bitmap转化为1位黑白像素图像(仅保留黑色,其它颜色会删除)
    vos3000外呼系统如何修改话机注册端口
    Camunda 7.x 系列【50】任务服务 TaskService
    迅捷 FW300R固件升级参考
    【系统架构设计】 架构核心知识: 2 云原生架构
  • 原文地址:https://blog.csdn.net/buptsd/article/details/126101269