• 进程控制,父子进程


    学习重点

    1. 进程控制

      主要需要通过进程控制回答以下问题:

      • 在OS的支持下,进程具体是如何启动起来的?

        详细介绍程序是如何运行起来,演变为进程的

        • 也就是通过exec加载程序后,进程启动起来的具体过程

        学完本部分就会知道:

        双击快捷图标,或者在命令行执行./a.out后,程序运行起来的背后原理

      • 有OS支持时,main函数return,或者使用exit、_exit所返回的返回值到底给了谁,有什么意义

      为什么在平时,这个返回值我们会感觉到并没有什么用处

      • java的进程是如何运行起来的

      程序在内存中运行起来后就演变为了进程

      • 但是由于java需要虚拟机来解释执行,中间夹一个虚拟机,所以要单独理解java进程是如何产生的

      与java类似的C#、pyhton等的程序,都是相同的运行过程

      • 什么情况下的程序才会涉及到多进程呢

        不过一般我们写程序都是单进程

    2. 进程关系

    ​ 基于OS运行的进程很多,众多进程之间往往存在着一定的关系,在这一部分会具体介绍进程之间有哪些关系

    1. 守护进程

      OS启动起来以后,会有很多我们平时看不见的,但是一直在默默运行的后台进程,这些后台进程大多其实是守护进程

    进程控制

    • 对于程序员来说,编写程序时有各种语言的区分
    • 但是文字编码形式的程序一旦被转为机器指令被CPU执行时,对于CPU来说全都是一样的,没有任何区别
    • 所有程序(c++,java)在Linux OS上运行起来,演变为进程时,都有着相同的启动过程
    • 有关进程控制的API,可以用来加载运行所有语言编写的程序

    进程相关知识

    将程序代码从硬盘拷贝到内存上,在内存上动态运行的程序就是进程

    存储位置存在状态运行过程
    程序硬盘静态
    进程内存上,从硬盘拷贝的副本动态进程有生有死
    多进程并发运行

    有OS支持时,会有很多的进程在运行,这些进程都是并发运行

    并发运行:

    就是CPU轮换的执行

    当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复

    • 由于时间片很短,在宏观上我们会感觉到所有的进程都是在同时运行的
    • 但是在微观上cpu每次只执行某一个进程的指令

    当然我们这里说的单核cpu的情况

    多核CPU:并发与并行是同时存在的

    进程号

    OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数),这个编号就是PID

    PID用来唯一标识一个进程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S3FVtkIc-1667904736804)(/home/guojiawei/.config/Typora/typora-user-images/image-20221108152039707.png)]

    ​ 如果当前进程结束了,这个PID可以被可以被重复使用,但是所有“活着”的进程,它们的进程ID一定都是唯一的

    • 进程号在系统调用时可以作为传入参数或者返回值
    PID放在了哪里?

    PID放在了该进程的task_struct结构体变量中

    进程在运行的过程中:OS会去管理进程,这就涉及到很多的管理信息;

    OS为了管理进程,会为每一个进程创建一个task_struct结构体变量,里面放了各种的该进程的管理信息

    三个特殊进程0,1,2

    0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行

    这三个进程非常重要

    1. 进程 PID == 0

      • 这个进程被称为调度进程

      • 功能是实现进程间的调度和切换

        该进程根据调度算法,让CPU轮换的执行所有的进程

      • 当pc指向不同的进程时,cpu就去执行不同的进程,这样就能实现切换,同时保存被切换进程的现场

      这个进程怎么来的?
      这个进程就是由OS演变来的,OS启动起来后,最后有一部分代码会持续的运行,这个就是PID==0的进程。
      由于这个进程是OS的一部分,凡是由OS代码演变来的进程,都称之为系统进程。
      
      • 1
      • 2
      • 3
    2. 进程 PID == 1

      • 作用:

        1. 初始化
        2. 托管孤儿进程
        3. 原始父进程-----就是它是最父的进程,用来创建子进程

      初始化

      • 这个进程被称为init进程

      • 作用是:

        它会去读取各种各样的系统文件,使用文件中的数据来初始化OS的启动,让我们的OS进入多用户状态,也就是让OS支持多用户的登录

      • 这个进程不是OS演变来的,也就是说这个进程的代码不属于OS的代码,这个进程是一个独立的程序
      • 该PID==1的程序代码放在了/sbin/init下,当OS启动起来后,OS会去执行init程序,将它的代码加载到内存,这个进程就运行起来了
    3. 进程 PID == 2

    • 该进程是守护进程或精灵进程,精灵进程也叫守护进程
    • 作用:专门负责虚拟内存的请页操作

    当OS支持虚拟内存机制时,加载应用程序到内存时,并不会进行完整的代码拷贝,只会拷贝当前要运行的那部分代码,当这部分代码运行完毕后,会再拷贝另一部分需要运行的代码到内存中,拷贝时是按照一页一页来操作的,每一页大概4096字节,这就是换页操作

    与调度进程一样,也是一个系统进程,代码属于OS的一部分

    获取进程相关的ID函数
    #include 
    #include 
    //获取调用该函数进程的进程ID(哪个函数调用它,返回的就是哪个)
    pid_t getpid(void);
    //p:parent p:process  
    //获取调用该函数进程的父进程ID
    pid_t getppid(void);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    #include 
    #include 
    //获取调用该函数进程的用户ID(在哪个用户下调用该进程)
    uid_t getuid(void);
    //获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID
    uid_t getgid(void);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    返回值:返回各种ID值,不会调用失败,永远都是成功的

    #include 
    #include 
    #include 
    #include 
    int main(void){
        //pid_t getpid(void);
        pid_t pid=getpid();
        printf("%u\n",pid);
        //pid_t getppid(void);
        pid_t pid_parent=getppid();
        printf("%u\n",pid_parent);
        //uid_t getuid(void);
        uid_t uid=getuid();
        printf("%d\n",uid);
        //uid_t geteuid(void);
        uid_t gid=getgid();
        printf("%d\n",gid);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    父进程就是当前终端窗口bash,因为在这个窗口执行的。

    vi /etc/passwd

    guojiawei❌1000:1000:guojiawei,:/home/guojiawei:/bin/bash

    fork()

    程序运行的三个过程:

    (1)在内存中划出一片内存空间
    (2)将硬盘上可执行文件中的代码(机器指令)拷贝到划出的内存空间中
    (3)pc指向第一条指令,cpu取指运行

    当有OS时,以上过程肯定都是通过调用相应的API来实现的。

    在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec

    • fork:开辟出一块内存空间

    • exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了

      运行起来的进程会与其它的进程切换着并发运行

    #include 
    #include 
    //pid_t  无符号整型
    //从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程
    pid_t fork(void);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    复制后有两个结果:

    1. 依照父进程内存空间样子,原样复制地开辟出子进程的内存空间
    2. 由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的代码和数据和父进程完全相同

    复制父进程的主要目的:

    • 就是为了复制出一块内存空间

    • 只不过复制的附带效果是,子进程原样的拷贝了一份父进程的代码和数据

      事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的代码

       由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数
            "当然这个说法有些欠妥,但是暂且这么理解"
    	1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
    	2)子进程的fork,成功返回0,失败返回-1,errno被设置。
    
    • 1
    • 2
    • 3
    • 4
    #include 
    #include 
    #include 
    #include 
    int main(void){
        //pid_t fork(void);
        pid_t ret=fork();
        printf("%d,ret=%d\n",getpid(),ret);
        while(1);//为了能够查到进程pid,不然程序结束后会被杀死
        return 0;
    }
    /*
    guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 
    12648,ret=12649
    12649,ret=0
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    #include 
    #include 
    #include 
    #include 
    int main(void){
        pid_t ret=fork();
        /*区分父子进程*/
        if(ret >0){//父进程返回值是子进程pid,大于0
            printf("parent PID=%d\n",getpid());
            printf("parent ret=%d\n",ret);
        }
        else if(ret ==0){//子进程返回0
            printf("child PID=%d\n",getpid());
            printf("child ret=%d\n",ret);
        }
        printf("hello,world\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 
    parent PID=13154
    parent ret=13155
    hello,world
    child PID=13155
    child ret=0
    hello,world
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上述代码:父子进程各有一份拷贝,但是父子进程可以根据if条件各自做不同的事

    复制的原理
    • Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的对应的还是物理内存。
    • 复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存空间,里面放了与父进程一模一样代码和数据

    父进程在os上基于物理内存复制出一系列数据结构得到虚拟内存;子进程fork时候,完全复制父进程的代码和数据,而且在新开辟的内存中存储和父进程一样的内容

    此时只要通过exec在子进程中加载新的程序,就可以实现开辟内存的效果

    父子进程各自执行哪些代码
    int main(void){
        printf("before fork()\n");
        pid_t ret=fork();
        /*区分父子进程*/
        if(ret >0){//父进程返回值是子进程pid,大于0
            printf("parent PID=%d\n",getpid());
            printf("parent ret=%d\n",ret);
        }
        else if(ret ==0){//子进程返回0
            printf("child PID=%d\n",getpid());
            printf("child ret=%d\n",ret);
        }
        printf("after fork\n");
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    guojiawei@ubantu-gjw:~/Desktop/process_control$ ./a.out 
    before fork()
    parent PID=14206
    parent ret=14207
    after fork
    child PID=14207
    child ret=0
    after fork
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    父子进程代码都相同

    • 父进程会执行fork()之前的语句,子进程只会执行fork()之后的语句
    • 父子进程的返回值不同,会根据if条件选择性执行

    尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行

    解释子进程打印出:before fork()

    • 首先子进程复制了整个父进程程序,但是子进程不会执行fork()之前的语句
    • printf()是行缓冲,如果没\n,也就是没写满一行,会挤压在缓冲区
    • 这里的before fork()是直接从父进程复制的数据结构,然后随着第一个\n一起打印

    父子进程共享操作文件

    分为两种情况:

    • 情况1:父子进程分别独立打开同一个文件

    • 情况2:fork之前打开文件,也就是父进程事先打开,父子进程共享

    独立打开文件

    涉及到的两个进程是父子关系,独立打开同一文件实现共享操作

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main(void){
       pid_t ret=0;
       int fd=0;
       ret=fork();//创建子进程
       if(ret>0){//父进程操作
          fd=open("mm.txt",O_RDWR|O_CREAT,0664);
          write(fd,"hello\n",6);
       }
       else if(ret==0){//子进程操作
          fd=open("mm.txt",O_RDWR|O_CREAT,0664);
          write(fd,"world\n",6);
       }
       return 0;
    }
    /*
    执行结果:world-------子进程覆盖了父进程
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    原因:

    由于父子进程单独打开文件,所以父子进程各自的进程表中,存放着单独的文件表,笔尖位置相同,由于打开同一个文件,最终节点指针指向同一个V节点,从而发生了覆盖

    • 独立打开同一文件时,父子进程各自的文件描述符,指向的是不同的文件表

    • 如果不想相互覆盖,需要加O_APPEND标志。

    • #include 
      #include 
      #include 
      #include 
      #include 
      #include 
      int main(void){
         pid_t ret=0;
         int fd=0;
         ret=fork();//创建子进程
         if(ret>0){//父进程操作
            fd=open("mm.txt",O_RDWR|O_CREAT|O_APPEND,0664);
            write(fd,"hello\n",6);
         }
         else if(ret==0){//子进程操作
            fd=open("mm.txt",O_RDWR|O_CREAT|O_APPEND,0664);
            write(fd,"world\n",6);
         }
         return 0;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
    fork之前打开文件
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main(void){
       pid_t ret=0;
       int fd=open("mm.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
       ret=fork();//创建子进程
       if(ret>0){//父进程操作
          write(fd,"hello\n",6);
       }
       else if(ret==0){//子进程操作
          write(fd,"world\n",6);
       }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件

    像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同的“文件表”

    0,1,2文件:

    ​ 子进程的0 1 2这三个打开的文件描述符,其实也是从父进程那里继承过来的,并不是子进程自己去打开的,同样的父进程的0 1 2又是从它的父进程那里继承过来的,最根溯源的话,都是从最原始的进程哪里继承过来的

    init进程会去打开标准输入,标注输出、标准出错输出这三个文件,然后0 1 2分别指向打开的文件,之后所有进程的0 1 2,实际上都是从最开始的init进程那里继承而来的。

    子进程会继承父进程的属性

         	(1)用户ID,用户组ID
    		(2)进程组ID
    		(3)会话期ID
    		(4)控制终端
    		(5)当前工作目录
    		(6)根目录(/)
    		(7)文件创建方式屏蔽字(unmask)
    		(8)环境变量
    		(9)打开的文件描述符
    			等等
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    子进程独立的属性:
    (1)进程ID
    (2)不同的父进程ID
    (3)父进程设置的锁,子进程不能被继承

  • 相关阅读:
    Linux 报错:failed to run command ‘java’: No such file or directory
    《算法导论》学习(十六)----一文讲懂红黑树
    智汇华云|基于TPM2.0的windows11虚拟机实践
    基于SSM的点餐平台系统设计与实现
    NAT模式LVS负载均衡群集部署
    用HTTP proxy module配置一个简单反向代理服务器
    python反爬⾍策略应对
    第五章-项目范围管理
    【无标题】This project has been opened by another efinity instance
    webpack优化篇(四十九):使用 webpack 进行图片压缩
  • 原文地址:https://blog.csdn.net/weixin_47173597/article/details/127756425