• 进程控制(粗略概括)


    1. 有关进程

    1.1 什么是进程

    我们在前面的课程就讲过这个问题,我们这里再来回顾下。

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

    对比一下进程和程序:

    存储位置 存在状态 运行过程
    程序 硬盘 静态的 无运行的过程
    进程 存在内存中,它是从磁盘上的程序考过来的副本 动态的 有运行的过程,所以进程有生有死


    1.2 多进程并发运行

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

    什么是并发运行?
    就是CPU轮换的执行,当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复,由于时间片很短,
    在宏观上我们会感觉到所有的进程都是在同时运行的,但是在微观上cpu每次只执行某一个进程的指令。

    图:


    当然我们这里说的单核cpu的情况,如果cpu是多核的话,不同的cpu核可以同时独立的执行不同的进程,这种叫并行运行。

    所以当cpu是多核时,并发与并行是同时存在的。


    1.3 进程ID(PID)

    1.3.1 什么是PID
    基于OS运行的进程有很多,OS为了能够更好地管理进程,为每个进程分配了一个唯一的编号(非负整数),这个编号就是PID,
    P就是process——进程的意思。

    这记好比公安局给每个人分配了一个唯一的身份证号(ID)是一样的。
    ps查看:


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

    因为ID的唯一性,当我们想创建一个名字唯一的文件时,往往可以在文件名中加入PID,这样就能保证文件名的唯一性。


    1.3.2 那么PID放在了那里呢?

    进程在运行的过程中,OS会去管理进程,这就涉及到很多的管理信息,OS(Linux)为了管理进程,会为每一个进程创建一个
    task_struct结构体变量,里面放了各种的该进程的管理信息,比如第一章介绍的文件描述符表,又比如我们这里讲的PID。

    所以PID放在了该进程的task_struct结构体变量中,有关task_struct在前面的课程就介绍过,相信大家不会陌生。


    1.3.3 如何获取PID呢?
    后面回答这个问题。





    1.4 三个特殊的进程

    OS运行起来后有三个特殊的进程我们需要了解下,他们的PID分别是0、1、2。

    0、1、2这个三个进程,是OS启动起来后会一直默默运行的进程,直到关机OS结束运行,尽管我们总是忽略它们的存在,但是它们
    确非常的重要。



    1.4.1 进程 PID == 0 的进程

    (1)作用
    这个进程被称为调度进程,功能是实现进程间的调度和切换,该进程根据调度算法,该进程会让CPU轮换的执行所有的进程。

    怎么实现的?
    当pc指向不同的进程时,cpu就去执行不同的进程,这样就能实现切换。



    (2)这个进程怎么来的
    这个进程就是有OS演变来的,OS启动起来后,最后有一部分代码会持续的运行,这个就是PID==0的进程。

    由于这个进程是OS的一部分,凡是由OS代码演变来的进程,都称之为系统进程。



    1.4.2 进程ID == 1的进程

    (1)作用
    1)作用1:初始化
    这个进程被称为init进程,这个进程的作用是,他会去读取各种各样的系统文件,使用文件中的数据来初始化OS的启动,
    让我们的OS进入多用户状态,也就是让OS支持多用户的登录。

    2)作用2:托管孤儿进程
    什么事孤儿进程,怎么托管的,有关这个问题后面会详细介绍。

    3)作用3:原始父进程

    原始进程————>进程————————>进程————————>终端进程——————>a.out进程
    | | |
    | | |
    V V |
    进程 进程 进程
    | | |
    | | |
    ... ... ...



    (2)这个进程怎么运行起来的
    这个进程不是OS演变来的,也就是说这个进程的代码不属于OS的代码,这个进程是一个独立的程序,程序代码放在了
    /sbin/init下,当OS启动起来后,OS回去执行init程序,将它的代码加载到内存,这个进程就运行起来了。



    1.4.3 进程ID == 2的进程

    (1)作用
    页精灵进程,专门负责虚拟内存的请页操作。

    疑问:什么精灵进程?
    精灵进程也叫守护进程,我们后面讲到“守护进程”这一章时,你自然就知道了。


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

    想了解详细换页操作的同学,请看《计算机体系结构》软件篇4——操作系统部分的课程。


    (2)这个进程怎么运行起来的
    与调度进程一样,也是一个系统进程,代码属于OS的一部分。



    1.5 获取与进程相关的各种ID的函数

    1.5.1 函数原型和所需头文件
    #include <sys/types.h>
    #include <unistd.h>

    pid_t getpid(void);
    pid_t getppid(void);
    uid_t getuid(void);
    gid_t getgid(void);


    (1)功能
    1)getpid函数:获取调用该函数进程的进程ID。
    2)getppid函数:获取调用该函数进程的父进程ID,第一个P是parent,第二个process。
    3)getuid函数:获取调用该函数进程的用户ID。
    在什么用户下运行的该进程,得到的就是该用户的用户ID,查看/etc/passed文件,可以找到该UID对应的用户名。

    4)getgid函数:获取用户组的ID,也就是调用该函数的那个进程,它的用户所在用户组的组ID。


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


    1.5.2 代码演示



    2. 程序的运行过程


    2.1 程序如何运行起来的

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


    当有OS时,以上过程肯定都是通过调用相应的API来实现的。
    在Linux下,OS提供两个非常关键的API,一个是fork,另一个是exec。

    fork:开辟出一块内存空间
    exec:将程序代码(机器指令)拷贝到开辟的内存空间中,并让pc指向第一条指令,CPU开始执行,进程就运行起来了
    运行起来的进程会与其它的进程切换着并发运行。


    2.2 fork
    2.2.1 函数原型
    #include <unistd.h>

    pid_t fork(void);

    为了便于大家更容易的理解,我们在介绍fork时会适当的隐去一些信息,所以虽然不能保证100%是正确的,但是我们能够向
    大家解释清楚fork函数的作用。


    (1)函数功能
    从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程。

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

    其实复制父进程的主要目的,就是为了复制出一块内存空间,只不过复制的附带效果是,子进程原样的拷贝了一份
    父进程的代码和数据,事实上复制出子进程内存空间的主要目的,其实是为了exec加载新程序的代码。


    (2)函数参数:无参数。
    (3)函数返回值
    由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数,当然这个说法有些欠妥,但是暂且这么理解。

    1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
    2)子进程的fork,成功返回0,失败返回-1,errno被设置。


    (4) 代码演示



    如何让父子进程做不同的事情?






    2.2.2 说说复制的原理
    Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的
    对应的还是物理内存。

    复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存
    空间,里面放了与父进程一模一样代码和数据,
    图:



    如果想了解什么是虚拟内存,请看《计算机体系结构》软件篇4——操作系统。



    2.2.3 父子进程各自会执行哪些代码

    复制出子进程后,父子进程各自都有一份相同的代码,而且子进程也会被运行起来,那么我们来看一下,父子进程各自会执行
    哪些代码。

    图:


    代码验证:


    (1)父进程

    1)执行fork前的代码
    2)执行fork函数
    父进程执行fork函数时,调用成功会返回值为子进程的PID,进入if(ret > 0){}中,执行里面的代码。
    if(ret > 0){}中的代码只有父进程才会执行。

    3)执行fork函数后的代码


    (2)子进程
    1)fork前的代码
    尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行。

    2)子进程调用fork时,返回值为0,注意0不是PID。
    进入if(ret == 0){},执行里面的代码。

    if(ret == 0){}中的代码只有子进程执行。

    3)执行fork后的代码





    (3)验证子进程复制了父进程的代码和数据
    演示:



    2.3 父子进程共享操作文件

    (1)情况1:独立打开文件
    多个进程独立打开同一文件实现共享操作,我们在第1章讲过,不过那时涉及到的多个进程是不相干进程,而现在我们这里要讲
    的例子,里面所涉及到的两个进程是父子关系,不过情况是一样的。

    1)代码演示




    2)文件表结构
    图;





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

    因为拥有不同的文件表,所以他们拥有各自独立的文件读写位置,会出现相互覆盖情况,如果不想相互覆盖,
    需要加O_APPEND标志。





    (2)情况2:fork之前打开文件


    1)代码演示


    2)文件表结构
    图:



    子进程会继承父进程已经打开的文件描述符,如果父进程的3描述符指向了某个文件,子进程所继承的文件描述符3也会指向这个文件。
    像这种继承的情况,父子进程这两个相同的“文件描述符”指向的是相同的“文件表”。

    由于共享的是相同的文件表,所以拥有共同的文件读写位置,不会出现覆盖的情况。


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

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


    init 012 012 012 012 012 012
    原始进程————>进程————————>进程———>...———>终端进程——————>a.out进程——————>a.out进程
    | | |
    | | |
    V V V
    进程012 进程012 进程012
    | | |
    | | |
    ... ... ...


    2.4 子进程会继承父进程的哪些属性

    2.4.1 子进程继承如下性质

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

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

    3. exec加载器

    exec加载器就是我们之前介绍的加载函数。

    3.1 exec的作用


    父进程fork复制出子进程的内存空间后,子进程内存空间的代码和数据和父进程是相同的,这样没有太大的意义,我们需要在子进
    程空间里面运行全新的代码,这样才有意义。

    怎么运行新代码?
    我们可以在if(ret==0){}里面直接写新代码,但是这样子很麻烦,如果新代码有上万行甚至更多的话,这种做法显然是不行的,因此
    就有了exec加载器。

    有了exec后,我们可以单独的另写一个程序,将其编译好后,使用exec来加载即可。


    3.2 exec函数族

    exec的函数有很多个,它们分别是execve、execl、execv、execle、execlp、execvp,都是加载函数。
    其中execve是系统函数,其它的execl、execv、execle、execlp、execvp都是基于execve封装得到的库函数,因此我们这里重点介绍
    execve函数,这个函数懂了,其它的函数原理是一样的。


    3.2.1 execve函数原型

    #include <unistd.h>

    int execve(const char *filename, char **const argv, char **const envp);


    (1)功能:向子进程空间加载新程序代码(编译后的机器指令)。

    (2)参数:
    1)filename:新程序(可执行文件)所在的路径名

    可以是任何编译型语言所写的程序,比如可以是c、c++、汇编等,这些语言所写的程序被编译为机器指令后,
    都可以被execve这函数加载执行。

    正是由于这一点特性,我们才能够在C语言所实现的OS上,运行任何一种编译型语言所编写的程序。


    疑问:java可以吗?
    java属于解释性语言,它所写的程序被编译后只是字节码,并不是能被CPU直接执行的机器指令,所以不能被execve直接加
    载执行,而是被虚拟机解释执行。

    execve需要先加载运行java虚拟机程序,然后再由虚拟机程序去将字节码解释为机器指令,再有cpu去执行,在后面还会详细
    讨论这个问题。


    2)argv:传给main函数的参数,比如我可以将命令行参数传过去
    3)envp:环境变量表

    (3)返回值:函数调用成功不返回,失败则返回-1,且errno被设置。

    (4)代码演示




    命令行参数/环境表 命令行参数/环境表 命令行参数/环境表
    终端窗口进程——————————————————>a.out(父进程)——————————————————————>a.out(子进程)——————————————>新程序
    fork exec

    exec的作用:将新程序代码加载(拷贝)到子进程的内存空间,替换掉原有的与父进程一模一样的代码和数据,让子进程空间运行全
    新的程序。


    3.3 在命令行执行./a.out,程序是如何运行起来的
    (1)窗口进程先fork出子进程空间
    (2)调用exec函数加载./a.out程序,并把命令行参数和环境变量表传递给新程序的main函数的形参


    3.4 双击快捷图标,程序是怎么运行起来的
    (1)图形界面进程fork出子进程空间
    (2)调用exec函数,加载快捷图标所指向程序的代码
    以图形界面方式运行时,就没有命令行参数了,但是会传递环境变量表。




    4. system函数

    如果我们需要创建一个进子进程,让子进程运行另一个程序的话,可以自己fork、execve来实现,但是这样的操作很麻烦,
    所以就有了system这个库函数,这函数封装了fork和execve函数,调用时会自动的创建子进程空间,并把新程序的代码加载到
    子进程空间中,然后运行起来。

    虽然有system这函数,但是我们还是单独的介绍了fork和execve函数,因为希望通过这两个函数的介绍,让大家理解当有OS支持时,
    程序时如何运行起来的。

    4.1 system函数原型
    #include <stdlib.h>

    int system(const char *command);

    (1)功能:创建子进程,并加载新程序到子进程空间,运行起来。

    (2)参数:新程序的路径名

    (3)代码演示

    system(“ls”);

    system(“ls -al”);



    5. 回收进程资源

    进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源。


    5.1 为什么要回收进程的资源?

    (1)程序代码在内存中动态运行起来后,才有了进程,进程既然结束了,就需要将代码占用的内存空间让出来(释放)。

    (2)OS为了管理进程,为每个进程在内存中开辟了一个task_stuct结构体变量,进程结束了,那么这个结构体所占用的内存空间也
    需要被释放。

    (3)等其它资源


    5.2 由谁来回收进程资源
    由父进程来回收,父进程运行结束时,会负责释放子进程资源。





    5.3 僵尸进程和孤儿进程


    5.3.1 僵尸进程

    子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程。

    为什么子进程会变成僵尸进程?
    子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着资源不拉屎僵尸进程。

    就好比人死后不腐烂,身体占用的资源得不到回收是一样的,像这种情况就是所谓的僵尸。





    5.3.2 孤儿进程
    没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程。

    为了能够回收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid==1的init进程,每当被托管的子进程终止时,init会立即
    主动回收孤儿进程资源,回收资源的速度很快,所以孤儿进程没有变成僵尸进程的机会。




    5.3.3 演示
    (1)僵尸进程

    ps查看到的进程状态
    R 正在运行
    S 处于休眠状态
    Z 僵尸进程,进程运行完了,等待被回收资源。


    (2)孤儿进程

    佳嵌工作室

  • 相关阅读:
    助理需要看的书
    linux 磁盘管理以及维护
    转:工作与创业区别
    《编写可读代码的艺术》---把控制流变得可读
    Visual studio插件 Reshaper--- 常用快捷键
    为啥我喜欢在Windows 7环境下做Unity开发?
    《编写可读代码的艺术》---写出言简意赅的注释
    《编写可读代码的艺术》---该写什么样的注释
    《编写可读代码的艺术》---美观代码
    《编写可读代码的艺术》---不会误解的名字
  • 原文地址:https://www.cnblogs.com/lemaden/p/10422354.html
Copyright © 2020-2023  润新知