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;
      } 
      
    2. 输出:

      [root@localhost linux]# ./a.out
      Before: pid is 43676
      After:pid is 43676, fork return 43677
      After:pid is 43677, fork return 0
      
    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. 程序文件通过编译器被编译为可执行文件时,已经划分好了这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 $?							//打印最近一次进程退出时的进程退出码
    
  • 进程

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

      waitpid

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

      子进程退出status

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

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

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

    status构成

  • 查看终止信号:

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

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

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

    (status >> 8) & 0xff
    
  • 查看正常退出进程的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;
    }
    

宏查看$和信号:

  • 上述过程使用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"); 
    }
    

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");
    }
    

进程阻塞 & 进程非阻塞:

  • 进程阻塞:

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

  • 进程非阻塞:

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

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

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

//进程阻塞:
waitpid(id, &status, 0);

//进程非阻塞:
waitpid(id, &status, WNOHANG);
//W含义wait,NO含义没有,HANG含义阻塞
  • 进程非阻塞模式:

    1. 代码:

      #include <stdio.h>
      #include <unistd.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      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;
      }
      
    2. 输出:
      非阻塞等待

进程程序替换:

  • 背景:

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

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

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

  • 进程不变:

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

    PCB mm_struct 页表 都没有新建

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

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

进程程序替换函数:

  • 程序加载器:

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

    #include <unistd.h>`
    //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. 代码:
      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);
        
      2. v表示参数以数组形式传入

        char* argv[] = {
            "ls",
            "-a",
            "-i",
            "-l",
            NULL
        }
        execl("usr/bin/ls", argv);
        
execlp() & execvp():
  • 异同:

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

    2. 异:

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

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

        char* argv[] = {
            "ls",
            "-a",
            "-i",
            "-l",
            NULL
        }
        execvp("ls", argv);
        

execle() & execve():

  • 异同:

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

    2. 异:

      1. le以列表携带参数:

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

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

    1. 函数:

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

    3. 输出:

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

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

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

starnight531

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

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

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

打赏作者

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

抵扣说明:

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

余额充值