zoukankan      html  css  js  c++  java
  • 进程控制(粗略概括)

    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)孤儿进程

    佳嵌工作室

  • 相关阅读:
    _ 下划线 Underscores __init__
    Page not found (404) 不被Django的exception中间件捕捉 中间件
    从装修儿童房时的门锁说起
    欧拉定理 费马小定理的推广
    线性运算 非线性运算
    Optimistic concurrency control 死锁 悲观锁 乐观锁 自旋锁
    Avoiding Full Table Scans
    批量的单向的ssh 认证
    批量的单向的ssh 认证
    Corrupted MAC on input at /usr/local/perl/lib/site_perl/5.22.1/x86_64-linux/Net/SSH/Perl/Packet.pm l
  • 原文地址:https://www.cnblogs.com/lemaden/p/10422354.html
Copyright © 2011-2022 走看看