• Linux---进程(1)


    操作系统

    传统的计算机系统资源分为硬件资源和软件资源。硬件资源包括中央处理器,存储器,输入设备,输出设备等物理设备;软件资源是以文件形式保存在存储器上的成熟和数据等信息。

    操作系统就是计算机系统资源的管理者。

    如果你的计算机没有安装操作系统,那么你将面对的是0,1代码和一些难懂的机器指令,通过按钮或者按键来操作计算机,这样笨拙+费时。安装操作系统之后,你面对的就不再是笨拙的裸机,而是操作便利,服务周到的操作系统,从而明显的改善了用户界面。

    操作系统为了保证自己的安全,也为了保证给用户能够提供服务,就以接口的方式给用户提供调用的入口,来获取操作系统内部的数据。

    这个接口是操作系统提供的用C语言实现的,自己内部的函数调用---系统调用

    所有访问操作系统的行为,都只能通过系统调用完成,因为操作系统不会让你随意的去对自己进行更改。

    所以,操作系统就是一个管理者,利用操作系统能够有效的组织和管理系统中的各种软硬件资源,合理的组织计算机系统工作的流程,控制程序的执行,并且向用户提供一个良好的工作环境和友好的接口。(程序猿通过暴露出的接口,开发出来各种软件供普通用户使用)。

    那么操作系统是如何管理的呢?

    在学校中,我们就是最典型的被管理者,校长是管理者。管理者与被管理者是不需要见面的,查考勤情况的活肯定不是校长监督的吧。那么校长连A同学都不知道是谁,怎么管理好这么多同学呢?

    其实只要校长拿到了学生的数据,就能进行管理,见不见面不是必须的,就算见面了,也是为了获取某同学的数据。管理的本质:是通过对数据的管理达到对人的管理

    但是不见面的情况下,校长是怎么知道A同学挂没挂科呢?这些数据都可以通过辅导员来拿到数据。

    在这里,校长相当于操作系统,辅导员相当于驱动程序,学生相当于软硬件资源。所以操作系统要管理好软硬件资源是通过获取硬件的各种状态数据来进行管理,这个数据从驱动程序中获得。一个硬件不能用了,驱动程序把信息传递给操作系统,操作系统告知用户。

    学校中的学生很多,他弄了一个excel表格,让辅导员按照这个表格获取学生的信息,辅导员获取完信息后,在把表格给校长,现在校长要找谁个子最高等信息,只需要遍历一遍表格即可。这个过程就是一个描述的过程。

    但校长曾经是一个程序猿,他弄了一个结构体来实现这个表格。

    1. struct student
    2. {
    3. char 学院[];
    4. char 专业[];
    5. ......
    6. struct student *next;
    7. }
    8. struct student stu1 = {};

    每一个结构体对象里面存着学生的信息,通过next来对学生进行链接。

    所以校长只要把这个学生链表管理好就行了。这样就将对学生的管理工作变成了对链表的增删查改。想找挂科超过3科的,直接遍历链表即可。填写学生信息的过程是描述过程,把学生通过节点链接起来的过程是组织的过程。

    操作系统中,管理任何对象,最终就变成了对某种的数据结构的管理。操作系统管理的过程跟上面的例子一样:先描述在组织。

    之前写通讯录之类管理系统,先把要存的信息写在结构体中,然后对这个结构体进行封装,这不就是先描述,在组织吗?

    1. struct person
    2. {
    3. char name;
    4. int age;
    5. char telphone1;
    6. char telphone2;
    7. ...
    8. }
    9. struct contact
    10. {
    11. struct person[100];
    12. int num;
    13. ...
    14. }

    系统调用和库函数概念

    操作系统不相信任何人,只会提供系统调用接口,如果你想简介的访问硬件或者打开文件之类的操作,是不能直接访问底层硬件,而是层层访问,以贯穿的形式。比如说printf函数,他是C标准函数,他的底层绝对要封装系统调用接口,所以C/C++封装的库函数,和系统调用接口的关系,是上下层被调用的关系,而不是直接绕过系统调用接口直接访问的。

    那么谁在上,谁在下呢?

    库函数在上,系统调用接口在下,


    • 在开发的角度,操作系统对外表现为一个整体,但是会暴露一部分接口,供程序猿开发使用,这部分由操作系统提供的接口,叫做系统调用
    • 系统调用在使用上, 功能比较基础,对用户的要求也相对较高,所以,有些开发者可以对部分系统调用进行适度封装,从而形成了库,有了库,就很利于上层用户或者开发者进行二次开发

    进程

    一个已经加载到内存中的程序,叫做进程。

    这些已经打开的软件,都是进程,对上面的某一个进程,右键结束任务,就杀死了某个进程。

    在Linux中通过ps ajx可以查看所有的进程。

    这些进程都有自己的名字和编号。

    top命令,查看正在运行的进程。

    现在在Linux中写一段代码

    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. while (true)
    6. {
    7. std::cout << "This is a process." << std::endl;
    8. sleep(1);
    9. }
    10. return 0;
    11. }

    在运行这段代码之后,输入指令查看进程。


    一个名为proc的程序,是保存在磁盘当中的,在计算机开机的时候,操作系统一定是预先加载到内存中的。如果我想把proc运行,根据冯诺依曼结构,这个proc一定会先加载到内存中,是数据的部分,交给控制器;是二进制的部分交给运算器去执行。

    但是一个操作系统可以同时运行多个进程,那么操作系统中可能会存在刚打开的进程,正在运行的进程,即将结束的进程,所以操作系统要将这些进程进行管理起来。要想管理,得先让操作系统认识这些进程,也就是先描述起来,然后对这些进程进程管理,也就是组织。

    描述进程

    任何一个进程在加载到内存的时候,形成真正的进程时,操作系统要先创建描述进程的结构体对象---PCB Process Ctrl Block(进程控制块)

    那么PCB是什么呢??

    要想认识某个事物,就要先知道它的属性,比如说,三边长度一样的封闭图形等。当属性堆积的够多的时候,这些属性集合起来,就是目标对象。

    所以描述进程就是将最常用的属性放在一起。PCB就是进程属性的集合。

    而进程就是PCB+程序和数据组成的。

    PCB的内容

    信息

    含义

    进程标识符

    表明系统中的各个进程

    状态

    说明进程当前的状态

    位置信息

    指名程序及数据在主存或外存的物理位置

    状态信息

    参数,信号量,消息等

    队列指针

    链接同一状态的进程

    优先级

    进程调度的依据

    现场保护区

    将处理机的现场保护到该区域,以便再次调度时能继续正确运行

    其他

    因不同的系统而异


    程序:程序部分描述了进程需要完成的功能。

    数据:数据部分包括程序执行时所需的数据及工作区,该部分只能为一个进程所专用,是进程的可修改部分。


    所以将程序加载到内存中,不光光是把代码和数据放到操作系统中,还会创建用来描述这个程序的PCB对象。

    PCB中存在指针信息,用来找到自己的代码和数据。

    系统当中不会只有一个进程,会有多个进程。这些进程,加载到内存中,在创建PCB对象,PCB中存在指针,通过指针再将所有的进程的PCB链接在一块,在通过PCB中的指针,找到自己的代码和数据。这样在操作系统中,对进程的管理,就变成了对单链表进行增删查改!


    上面所描述的,是所有操作系统的原理,但是具体的操作系统会有差别。

    那么Linux操作系统是怎么做的?

    在Linux操作系统下的PCB是 tack_struct

    tack_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

    task_struct内容分类

    跟上面的PCB的组成一样。


    PCB -> tack_struct 结构体,里面包含进程的所有属性。

    Linux中如何组织进程,Linux内核中,最基本的组织进程tack_struct的方式是采用双向链表组织的。

    但这并不是存粹的双链表,可能task_struct中存在指针,指向一个队列,一棵树等其他数据结构中。Linux中数据结构的关系,一定是复杂的。

    对tack_struct的管理,就是放在某个组织的数据结构中,这个进程要等待,放在等待队列里,要运行,放在运行队列里,所以这个进程要怎么工作,就放在哪一个在组织的数据结构中。


    ps ajx | head -1 && ps ajx | grep ./proc通过这个命令,就可以查看我们刚刚执行的那个名为proc的程序的进程

    也可以通过 proc去查看。ls /proc

    这个目录会在关机的时候,全部关闭,再开机的时候,再打开。

    这些蓝色的都是目录,在这里都是以PID(唯一标识符)的形式表示的

    ./proc的PID是9965,既然蓝色的是目录,说明就可以进入到这个目录中查看一些信息。

    当我ctrl+c结束这个进程的时候,再次查看这个进程,就会找不到。如果再次启动这个进程,PID就会发生变化。

    第二个gerp.../proc的进程是grep ./proc的进程,因为进行过滤的时候系统创建出了这个进程。


    这个exe文件是一个链接文件,指向正在运行的名为proc的进程。

    cwd是当前进程的工作目录。


    再使用 touch命令的时候,直接再某一个目录上 touch为什么能够直接创建出文件,而不用先输入路径呢?因为再使用 touch命令的时候,系统自动创建进程,进程中的cwd会自动记录当前目录的路径。再创建的时候自动的把cwd拼接到要创建的文件名前面。 cwd/FileName

    通过系统调用获取进程标识符

    每个进程要被管理,就必须有一个唯一的标识符---PID

    COMMAND代表进程执行的时候,是什么命令。


    那么这个PID有什么命令呢?

    现在我写了一个死循环

    代码再不停的打印Hello World,我通过命令查看这个进程的PID

    然后现在我要杀死这个进程

    kill -9 PID

    查到某个进程的PID,然后kill可以直接干掉。


    如何查询自己的PID呢?

    通过getpid函数可以.

    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. while (true)
    6. {
    7. std::cout << "I am " << getpid() << std::endl;
    8. std::cout << "Parent am " << getppid() << std::endl;
    9. }
    10. return 0;
    11. }

    PID是自己本身的,PPID是父进程的PID。

    这个程序我执行了两次,PID一直在变,PPID却没有变化,这个PPID是什么呢?

    当我们执行程序的时候,bash会给我们创建进程。我们再命令行中输入的所有指令,都是bash进程的子进程。

    通过系统调用创建进程-fork

    如果我们自己想创建进程,fork可以完成

    根据这个返回值来看,这个函数有两个返回值??

    1. #include <unistd.h>
    2. #include <stdio.h>
    3. int main()
    4. {
    5. printf("begin:This is a process. pid:%d,ppid:%d",getpid(),getppid());
    6. pid_t id = fork();
    7. if (id == 0)
    8. {
    9. while (true)
    10. {
    11. printf("This is a child process. pid:%d,ppid:%d",getpid(),getppid());
    12. sleep(1);
    13. }
    14. }
    15. else if (id > 0)
    16. {
    17. while (true)
    18. {
    19. printf("This is a person process. pid:%d,ppid:%d",getpid(),getppid());
    20. sleep(1);
    21. }
    22. }
    23. else
    24. {
    25. printf("error");
    26. }
    27. else
    28. {
    29. std::cout << "error" << std::endl;
    30. }
    31. return 0;
    32. }

    按照我们以前写的程序,只要第一个if条件满足,就不会再执行其他的分支语句了。

    这个代码再循环打印子进程和父进程中的内容,一份代码中跑了两个死循环,再之前是不可能实现的,但是有了fork就可以了,因为变成了两个进程--父进程和子进程。

    父进程的pid是3870,子进程的ppid也是3870,说明这两个进程之间的关系是父子。

    这个2592就是bash

    说明父进程是通过bash来创建的。

    上面的代码执行到fork的时候,创建出了子进程。


    代码是从上往下执行的,执行到fork函数的时候,整个代码就会变成两个执行流,一个进入到fork大于0的代码中,一个进入到fork==0的代码中。这是能跑两个代码的原因。


    为什么fork要给子进程返回0,给父进程返回子进程pid?

    一般而言,fork之后的代码父子共享。返回不同的返回值,是为了区分,让不同的执行流,执行不同的代码块。


    一个函数是如何做到返回两次的?

    fork是一个函数,函数有自己的实现方法,它本身是在操作系统中,有自己的实现。

    return也是代码,父进程执行到return的时候会返回一次,子进程执行到return的时候也会返回一次,所以就返回了两次。


    fork有两个返回值,那么一个id变量怎么会有两个值呢?

    父进程在执行的时候,会有自己的代码和数据,这个代码是和子进程共享的,数据呢?父进程有自己的数据的,子进程也应该有自己的数据。进程之间是有独立性的,一个进程崩了,不会影响另一个进程。所以子进程要想办法把父进程的数据拷贝一份,拷贝一份各有各的数据,但是子进程也有可能不会对一些数据进行访问,没用的数据也进行拷贝,会造成浪费。子进程要访问父进程中的数据,可以对要修改的数据,进行拷贝,改多少申请多少空间,这种技术是数据层面的写时拷贝。如果没有更改,父子进程的代码和数据共享。


    上面说过,进程=PCB+代码和数据。创建子进程就是系统中多了一个进程。父进程中的PCB存在指针指向代码和数据,而子进程中也会存在指针,指向代码和数据,父子进程中的代码是共享的。fork之后,父子进程代码共享,但是可以做不同的事情。


    fork之后,父子进程谁先运行呢?

    这个是由调度器决定的。


    进程状态


    CPU只有一个的情况下,存在多个进程,这些进程要竞争CPU的资源,这些资源要合理的分配,所以CPU要维护一个运行队列(struct runqueue),这些进程要想运行,要先链接到运行队列当中,因为PCB本来就是数据结构对象,运行队列中的头指针指向PCB,尾指针指向最后一个PCB。这个时候CPU要运行进程,直接在运行队列中找到一个进程放到CPU中运行即可。

    凡是处于运行队列中的进程,都属于运行状态

    当一个进程在运行时,则该进程处于运行态

    这个时候你在创建一个进程,这个进程想要执行,链接在队列中即可。

    那么一个进程只要把自己放到CPU上开始运行了,是不是一直要执行完毕,才把自己放下来?

    不是的,比如平常写代码的时候,我们写了一个死循环,但是其他程序还能正常运行。每一个进程都有一个叫做时间片的概念,一旦运行时间超过时间片,这个进程就会重新去排队,CPU运行新的进程,所以在一段时间内,所有进程代码都会被执行。也可以称为并发执行


    在操作系统中,底层存在各种各样的硬件。操作系统可以管理软硬件资源。这些硬件虽然都不同,但他们都可以用结构体对象来表示。

    1. struct dev
    2. {
    3. int type;
    4. int status;
    5. struct task_struct *head;
    6. ....
    7. }

    将这些结构体在链接起来,这样就可用数据结构来管理这些硬件了。

    现在我们写了一个带 std::cin的程序,运行以后,设备要从键盘当中读取数据,但是这个时候我们不输入,此时这个进程就要等待从键盘上获取数据,那么这个队列就不能放在运行队列中,会放在等待队列(waitqueue)当中,直到从键盘上获取了数据。获取了数据之后,就会放到运行队列当中。

    所以这种在等待特定设备的进程,叫做阻塞状态。

    一个进程正在等待某一件事发生(例如请求I/O等待I/O完成等)而暂时停止运行


    挂起是一个比较奇怪的概念。

    假如现在有一个设备叫做磁盘,如果今天有多个进程正在等待键盘资源,这时操作系统内部的内存资源严重不足了,操作系统会在保证正常的情况下,省出来内存资源,一些在等待队列当中的资源没有被CPU调用,这些进程处于空闲状态,此时操作系统会将这些进程的PCB保留,代码和数据会放到外设当中,这个过程叫做换出,PCB在等待队列中排队,等到这个进程就绪了,在把代码和数据从外设中拿出来,放到运行队列当中,这个过程叫做换入其中一个进程只有PCB在,代码和数据被换出了,此时这个进程的状态就是挂起状态,这样就给以给操作系统省出资源。


    Linux中的进程状态

    1. /*
    2. * The task state array is a strange "bitmap" of
    3. * reasons to sleep. Thus "running" is zero, and
    4. * you can test for combinations of others with
    5. * simple bit tests.
    6. */
    7. static const char * const task_state_array[] = {
    8. "R (running)", /* 0 */
    9. "S (sleeping)", /* 1 */
    10. "D (disk sleep)", /* 2 */
    11. "T (stopped)", /* 4 */
    12. "t (tracing stop)", /* 8 */
    13. "X (dead)", /* 16 */
    14. "Z (zombie)", /* 32 */
    15. };

    • R运行状态(running) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
    • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
    • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
    • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
    • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

    下面我们创建一个名为myproc.cpp的文件。

    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. while (true)
    6. {
    7. std::cout << "Hello World" << std::endl;
    8. sleep(1);
    9. }
    10. return 0;
    11. }

    运行这个进程之后在查看进程。

    发现,这个进程是S状态,myproc不是在运行吗?为什么会是S状态。

    现在把代码中的输出和sleep给去了。

    此时就变成了R状态。

    第一次的代码带有 std::cout 看似代码一直在打印,其实是阻塞状态,当我把输出给去了,就变成了运行态(R)。

    R+的意思是前台运行,在运行的时候给 ./myproc后面加上& => ./myproc &就变成后台运行。


    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. std::cout << "cin>>:";
    6. int a = 0;
    7. std::cin >> a;
    8. std::cout << "echo:" << a << std::endl;
    9. return 0;
    10. }

    在运行这个程序的时候,如果没有输入,界面就会一直卡在这里,等待着输入。

    查看进程

    我并没有输入,所以这个进程一直在等待,直到获取到相应的资源后,才可以运行,这就是S状态。所谓的阻塞,就是在等待资源。

    当获取了资源后,就是R状态了。


    如果一个进程处于D状态,在Linux中也是阻塞状态,也是深度睡眠。

    现在有一个进程,这个进程在向磁盘写入1G的数据,磁盘把数据接收后,存在某一区域,这个过程是要花时间的,在这期间,进程就要进行等待。完成这个操作后,磁盘向进程发起反馈,不管成功与否。那么在磁盘写入的时候,这个进程被操作系统强制干了,但是磁盘要向进程进行反馈的时候,却找不到进程了。这个数据若不重要,丢掉也无所谓;如果数据非常重要呢?丢失了就会造成大问题。所以让进程在等待磁盘写入完毕期间,这个进程不能被任何人干掉,不就行了。进程在等待磁盘写入期间,就是D状态---disk sleep


    t状态被称为暂停状态。

    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. while (true)
    6. {
    7. std::cout << "Hello World" << std::endl;
    8. sleep(1);
    9. }
    10. return 0; }

    运行之后,进程处于S状态

    通过kill -l可以查看更多的信号,之前用过kill -9 PID的命令用来杀死进程。

    18和19信号分别是继续和暂停信号。

    kill -19 PID

    在输入 kill -18 PID


    X是死亡状态。

    进程终止了,就要把资源进行回收,这只是一个返回状态,不会再任务列表里看到这个状态。


    Z状态也是僵尸状态。

    一个进程死掉了,并不会直接进行回收,而是先将进程的退出信息维持一段时间,让关心这个进程的人知道结果和原因。维护信息的这个状态,是Z状态。

    一个进程中,父进程最关心儿子进程的信息。如果父进程没有关心儿子进程,操作系统就会一直维持着儿子进程的信息。

    1. #include <iostream>
    2. #include <unistd.h>
    3. #include <cstdlib>
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if (id == 0)
    8. {
    9. int cnt = 5;
    10. while (cnt)
    11. {
    12. std::cout << "我是子进程,pid " << getpid() << "ppid" << getppid() << std::endl;
    13. cnt--;
    14. sleep(1);
    15. }
    16. exit(0);
    17. }
    18. else
    19. {
    20. while (true)
    21. {
    22. std::cout << "我是父进程,pid" << getpid() << "ppid" << getppid() << std::endl;
    23. sleep(1);
    24. }
    25. }
    26. return 0;
    27. }

    这个代码中,父进程并没有对子进程干任何事情,子进程结束之后,父进程就看着,啥也不干。

    当运行这个程序的时候,cnt为0前,一直是都是S状态,cnt为0后,父进程是S状态,子进程变成了Z状态,既僵尸状态。

    进程一般退出的时候,如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态。进程的相关资源尤其是task_struct 结构体不能被释放。僵尸进程会一直占用自身资源。僵尸进程不予回收就会造成内存泄漏问题。

    那么我们把myproc给结束之后,为什么不会变成僵尸进程?因为bash会对myproc负责。爹只对儿子负责。

    myproc结束,没有变成僵尸进程是因为bash会负责,那子进程为什么会没了?bash可不会对孙子负责。

    现在不让子进程退出,让父进程退出。

    1. #include <iostream>
    2. #include <unistd.h>
    3. #include <cstdlib>
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if (id == 0)
    8. {
    9. int cnt = 500;
    10. while (cnt)
    11. {
    12. std::cout << "我是子进程,pid " << getpid() << "ppid" << getppid() << std::endl;
    13. cnt--;
    14. sleep(1);
    15. }
    16. exit(0);
    17. }
    18. else
    19. {
    20. int cnt = 5;
    21. while (cnt)
    22. {
    23. std::cout << "我是父进程,pid" << getpid() << "ppid" << getppid() << std::endl;
    24. cnt--;
    25. sleep(1);
    26. }
    27. }
    28. return 0;
    29. }

    原来子进程的父进程的PID是15604,父进程退出之后,变成了1。

    如果父进程先退出,子进程的父进程会被改变成1号进程(操作系统)

    父进程是1号进程---孤儿进程,该进程被系统领养。

    父进程结束了,孤儿进程退出后,它的信息就没有人关心了。只能有操作系统来领养。

    所以上面子进程是僵尸进程,将父进程结束后,子进程由操作系统领养,所以子进程也会结束。


    僵尸进程的危害

    进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间。内存泄漏

    进程优先级

    cpu资源分配的先后顺序,就是指进程的优先权(priority)。优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可能改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能


    优先级是对于资源的访问,谁先访问,谁后访问。就好比买饭的时候要排队,我们已经进入了餐厅,有吃饭的权限,排队就是决定谁先吃,谁后吃的问题。

    为什么会有优先级呢?

    学生在学校要吃饭,如果给每个学生配一个厨师,那就不需要在餐厅排队吃饭了。但学生这么多,不可能实现每个学生配一个厨师。一个系统中有好多进程,这些进程不可能都配一个CPU,且资源是有限的,所以这些进程就需要对CPU进行竞争。操作系统必须保证大家良性竞争,就要确认优先级。如果不保证良性竞争,那结果就是谁nb谁资源多,这样会造成一些进程长时间得不到CPU资源,该进程的代码长时间无法得到推进---该进程的饥饿问题

    当然不要去修改优先级,调度器会自己解决优先级的问题。


    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. while (true)
    6. {
    7. std::cout << "This is a process." << std::endl;
    8. sleep(1);
    9. }
    10. return 0;
    11. }

    运行之后查看进程 ps -al

    PRI就是优先级(priority)
    NI就是nice值:进程优先级的修正数据。

    • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
    • 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
    • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为: PRI(new)=PRI(old)+nice
    • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值
    • nice其取值范围是-20至19,一共40个级别

    Linux不会让你随便的调整优先级,所以nice值给出的有限制。

    需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据


    如何更改优先级呢?

    top命令。

    然后输入r

    在输入PID,

    在输入新的nice值

    就可以完成修改了。

    如果新的nice值不在ni的区间,系统会自动的找在区间内的最大值。


    那么操作系统是如何根据优先级开展的调度呢?

    通过位图。可以近乎O(1)的时间复杂度来调度。


    其他概念

    • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
    • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
    • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
    • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

    环境变量

    环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性 。


    我们写的代码,在编译后要运行的时候,需要带 ./才能运行。而指令,则不需要带 ./就可以运行?指令和我们写的代码,都在目录中,这些目录还有什么区别吗?

    系统当中,针对于指令的搜索,Linux会提供一个环境变量PATH

    通过 echo $PATH可以打印环境变量

    这些路径以冒号为分隔符,在执行指令的时候,系统会在上面的路径中,寻找指令。所以我们自己写的代码,只写文件名,系统在路径中搜索不到,也就无法运行了。

    那么,将这个proc的路径,添加到PATH中,是不是就可以输入名字运行代码了呢?

    这样的写法是覆盖PATH,会将之前的路径都覆盖。在使用了之后绝大部分指令就不能运行了。因为系统找不到对应的路径。就算覆盖了,不用担心,PAHT会在启动的时候加载到内存,重启一下,就会恢复。

    将路径添加到PATH后,就可以输名字,运行代码了。


    输入 env可以查看环境变量。

    我们在终端输入指令的时候,会自动的记录上一条输入的指令,这个记录的指令是有限制的,HISTSIZE就是能够记录的条数

    USER代表的用户。

    PWD代表当前路径


    通过getenv函数可以获取环境变量。

    1. #include <iostream>
    2. #include <string>
    3. #include <stdlib.h>
    4. int main()
    5. {
    6. std::string who = getenv("USER");
    7. if (who == "root")
    8. {
    9. std::cout << "允许做任何事情" << std::endl;
    10. }
    11. else
    12. {
    13. std::cout << "你就一个普通用户,没有足够权限" << std::endl; }
    14. return 0;
    15. }

    环境变量是系统提供的一组name=value形式的变量,不同的环境变量有不同的用户,通常具有全局属性。

    命令行参数

    在学C语言的时候,一些教材上main函数是带参数的。

    1. int main(int argc, char *argv[])
    2. {
    3. }

    C/C++的main函数,是可以传参的,上面两个,就是参数。

    1. #include
    2. #include
    3. int main(int argc,char *argv[])
    4. {
    5. for (int i = 0; i < argc; i++)
    6. {
    7. printf("argv[%d]->%s",i,argv[i]);
    8. }
    9. return 0;
    10. }

    当我们运行代码的时候, 你以为你输入的是 ./proc -a -b -c,开始做命令行解释的时候,会把这个字符串打散成四个字符串,再将每个字符串的起始地址,保存在argv中,然后传递给main函数,argc中存数量,argv中存的字符串。

    为什么要这么做呢?

    1. #include <stdlib.h>
    2. #include <string.h>
    3. #include <stdio.h>
    4. int main(int argc, char *argv[])
    5. {
    6. if (argc != 2)
    7. {
    8. printf("Usage: %s -[a|b|c|d]\n",argv[0]);
    9. }
    10. if (strcmp(argv[1],"-a") == 0)
    11. {
    12. printf("aaa\n");
    13. }
    14. else if (strcmp(argv[1],"-b") == 0)
    15. {
    16. printf("bbb\n");
    17. }
    18. else if (strcmp(argv[1],"-c") == 0)
    19. {
    20. printf("ccc\n");
    21. }
    22. else if (strcmp(argv[1],"-d") == 0)
    23. {
    24. printf("ddd\n");
    25. }
    26. else
    27. {
    28. printf("defaul\n");
    29. }
    30. return 0;
    31. }

    同一个指令,不同的选项,可以得到不同的结果。

    这样可以为指令,工具,软件等提供命令行选项的支持。比如 ls -a,ps ajx等。


    还可以带第三个参数---环境变量列表。

    1. int main(int argc,char *argv[],char *env[])
    2. {
    3. }

    我们所运行的进程,都是子进程,bash本身在启动的时候,会从操作系统的配置文件中读取环境变量信息,子进程会继承父进程交给我的环境变量。

    怎么证明这件事?

    在命令行上这样搞,并不是环境变量,但确实存在。

    这是本地变量。

    所谓的本地变量就是在命令行中定义变量。本地变量不会被继承
    a = 1
    b = 2
    c = 3

    echo $a可以输出a。但如果敲一个代码看这个变量是无法找到的,echo却可以输出。

    两批命令:
    常规命令:通过创建子进程完成的。
    内建命令:bash不创建子进程,而是由自己亲自执行,类似于bash调用了自己写的,或者系统提供的函数。

    set命令可以查看系统当中的所有变量。

    现在将这个变量定义为环境变量。

    只需要带个export将它导出即可。

    在运行代码后,发现,会继承。

    unsert MY_VALUE会取消环境变量。


    进程地址空间

    在学习C/C++的时候经常提到这些空间,栈区,堆区,静态区等等,下面这张图也没少见到。

    1. #include
    2. #include
    3. int g_val_1;
    4. int g_val_2 = 100;
    5. int main()
    6. {
    7. printf("code addr: %p\n", main);
    8. const char* str = "Hello World";
    9. printf("read only string addr: %p\n", str);
    10. printf("init global value addr: %p\n", &g_val_2);
    11. printf("uinit global value addr: %p\n", &g_val_1);
    12. char *mem = (char*)malloc(100);
    13. printf("heap addr: %p\n", mem);
    14. printf("stack addr: %p\n", &str);
    15. return 0;
    16. }

    这是在Linux中敲的代码。

    这是运行结果,通过结果来看,是符合上面画的图的。不同的数据类型存储在不同的地方。


    栈区向下增长,堆区向上增长。

    通过代码来验证上面的话。

    看,堆区向上增长,栈区向下增长。


    1. #include
    2. #include
    3. int g_val = 1;
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if (id == 0)
    8. {
    9. // 子进程
    10. while (1)
    11. {
    12. printf("i am child, pid: %d, ppid: %d, g_val: %d, &g_val: %d\n", getpid(), getppid(), g_val, &g_val);
    13. sleep(1);
    14. }
    15. }
    16. else
    17. {
    18. while (1)
    19. {
    20. printf("i am parent, pid: %d, ppid: %d, g_val: %d, &g_val: %d\n", getpid(), getppid(), g_val, &g_val);
    21. sleep(1);
    22. }
    23. }
    24. return 0;
    25. }

    现在对代码进行一个小小的更改。

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. int g_val = 1;
    4. int main()
    5. {
    6. pid_t id = fork();
    7. if (id == 0)
    8. {
    9. // 子进程
    10. int cnt = 5;
    11. while (1)
    12. {
    13. printf("i am child, pid: %d, ppid: %d, g_val: %d, &g_val: %d\n", getpid(), getppid(), g_val, &g_val);
    14. sleep(1);
    15. if (cnt) cnt--;
    16. else
    17. {
    18. g_val = 200;
    19. printf("exchange g_val 1 -> 200");
    20. cnt--;
    21. }
    22. }
    23. }
    24. else
    25. {
    26. while (1)
    27. {
    28. printf("i am parent, pid: %d, ppid: %d, g_val: %d, &g_val: %d\n", getpid(), getppid(), g_val, &g_val);
    29. sleep(1);
    30. }
    31. }
    32. return 0;
    33. }

    在子进程中 g_val修改了,但是父进程中的却没有改变,确实,前面说过,不同的进程中数据不会有影响,但是看 g_val 的地址,父子进程都一样,地址都一样,值却不一样。

    这里可以得出一个小结论:如果变量的地址是物理地址,不可能存在上面的现象。这个地址是线性地址&&虚拟地址,我们平常写的C/C++用的指针,指针里面的地址,全部都不是物理地址。

    当我们运行一个程序后,这个程序会变成一个进程,内核一定会为这个进程创建PCB,同时也会创建一个进程地址空间。就是上面画的图。

    平常在写代码的时候,我们所用的空间就是全0~全F,进程地址空间是内核为进程创建的结构体对象,PCB里有指针指向该进程地址空间。通过一个页表(K-V的映射关系,K虚拟地址,V物理地址)的东西,这样就可以访问到物理地址了。

    父进程在创建子进程的时候,也会创建PCB结构,会以父进程为模板,创建出来,当然,子进程也会有自己的东西。子进程和父进程有独立性,子进程会从父进程中拷贝进程地址空间,页表也会拷贝,所以子进程跟父进程一样,通过页表的映射关系,通过虚拟地址,可以找到相同的物理地址。所以父子进程就实现了代码共享,数据共享。

    当子进程要数据进行修改的时候,会通过页表查地址,系统会发现,这个数据是和父进程共享的,系统就会重新为子进程分配一块空间,然后将修改后的变量放在这个新空间里,再将页表中虚拟地址所映射的物理地址进行一个更改。

    先经过写时拷贝 --- 是由操作系统亲自完成的。

    重新开辟空间,但是在这个过程中,左侧的虚拟地址是0感知的,不关心,不会影响它 g_val = 200。

    地址空间是什么

    你的地址总线排列组合形成地址范围[0, 2 ^ 32]

    地址空间就是在极端情况下,进程所能访问的地址边界情况。

    在小学的时候,同桌之间闹矛盾了,一般会画个三八线,三八线左面是小胖的位置,右边是小花的位置,小花画的三八线的本质就是 区域划分。那么用计算机语言如何描述小胖越界的情况呢? 所以 要有一个结构体来把这个空间给管理起来。

    1. struct area
    2. {
    3. int start;
    4. int end;
    5. };
    6. struct destop_area
    7. {
    8. struct area 小花;
    9. struct area 小胖;
    10. };

    小花说,你在越界,我就把三八线左移10cm。所谓的空间区域调整变大或者变小如何理解?

    1. struct destop_area line_area = {{1, 50}, {51, 100}};
    2. line_area.小胖.end -= 10;
    3. line_area.小花.start -= 10;

    这样,小胖的区域就小了10。

    小胖能把自己的东西放在自己的空间内。

    在范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被小胖直接使用。

    所谓的进程地址空间,本质是一个描述进程可视范围的大小,进程空间内一定要存在各种区域划分,对线性地址进行start和end即可。


    每一个进程都有自己的进程地址空间,在创建一个进程后,也要创建进程地址空间结构体对象,和PCB类似,地址空间也要被操作系统管理: 先描述 后组织。

    1. struct mm_struct
    2. {
    3. long code_start;
    4. long code_end;
    5. long readonly_start;
    6. long readonly_end;
    7. long init_start;
    8. long init_end;
    9. long uninit_start, uninit_end;
    10. long heap_start, heap_end;
    11. long stack_start, stack_end;
    12. };

    有一个身价十亿美金的富豪,有4个孩子,这四个孩子相互之间并不知道,富豪给四个孩子画了张大饼,孩子1要办工厂,往富豪要了5万美金。孩子2,3,4都往富豪要钱,他们都知道富豪有十个亿,只要自己要钱,富豪都会给,不管要了多少钱,他们都以为自己只是从富豪那里拿了一部分。等到富豪死了之后,四个孩子都以为自己会继承十个亿。

    这里 富豪就是操作系统,四个孩子就是 一个一个的进程。每一个进程都被操作系统画了张大饼,这张饼就是进程地址空间。

    页表里面其实是有一个标志位,表示对应的代码和数据是否已经加载到内存。

    在访问数据的时候,操作系统识别到对应的数据没有加载到内存里,这个时候操作系统会触发一个概念---缺页中断。

    当进程在被创建的时候,先创建内核数据结构,在加载对应的可执行程序。

    所以 为什么要有进程地址空间?

    1. 让进程以统一的视角看待内存。
    2. 增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。
    3. 因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行耦合。

    现代操作系统几乎不做任何浪费空间和浪费时间的事情。

    从现阶段而言 进程 = 内核数据结构(task_struct, mm_struct, 页表) + 程序的代码和数据

    进程控制

    一个进程中,父进程是要关心的。

    平常我们写的代码,为什么要在结尾加上return 0;

    0 是进程的退出码,由父进程接收。可以用return 的不同返回值数字,表示不同的出错原因

    main函数的返回值本质表示:进程运行完成时是否是正确的结果,如果不是,可以用不同的数字表示不同的出错原因。

    在用命令行敲一个代码之后,成功运行后,输入

    1. echo $?
    2. 保存最近一次进程退出的时候的退出码

    返回的这个数字,是让计算机看的。

    在linux中,有接口可以查看这些错误码。

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. int g_val = 1;
    5. int main()
    6. {
    7. int i = 0;
    8. for (i = 0; i < 200; i++)
    9. {
    10. printf("%d: %s\n", i, strerror(i));
    11. }
    12. return 0;
    13. }

    这是系统所提供的。


    这个报错,不就是strerror中的2号退出码吗?这个进程粗错误了,返回不同的错误码,以此来上计算机识别,计算机只懂数字,不懂其他的。


    如果不想用系统中的退出码,也可以自己设计。


    进程出了异常,本质是我们的进程收到了信号!

    现在我们写了一个死循环的进程,通过kill -9可以杀死该进程。



    之前我们写顺序表链表的时候,有时候会用exit来退出程序,其中exit(n),这个n是数字,也是退出码。

    exit在任意地方被调用表示调用进程退出。

    如果在show函数中用的是return,这代表的是该函数结束,并不是进程结束。


    有exit,return还有一个_exit,跟exit的区别是,_exit会直接结束进程,缓冲区的东西不在输出。exit在结束进程的时候,会把缓冲区的内存给输出。

    _exit()是系统调用接口,exit()先冲刷缓冲,关闭流等,在调用_exit()。

    我们的printf一定是先将数据写入缓冲区中,合适的时候,在进行刷新。

    进程等待

    通过系统调用wait/waipid,来进行对子进程进行状态检测与回收的功能。

    之前说过,在子进程退出的时候,父进程却不管不问,就可能会造成僵尸进程的问题,进而造成内存泄漏。进程一旦变成僵尸进程,就无法被杀死,西药通过进程等待来杀掉他,进而解决内存泄漏的问题---这个问题必须解决。

    我们要通过进程等待,获得子进程的退出情况---知道我布置给子进程的任务,他完成的怎么样了---可以关心,可以不关心---可选的。


    1. #include <iostream>
    2. #include <unistd.h>
    3. int main()
    4. {
    5. pid_t id = fork();
    6. if (id < 0)
    7. {
    8. perror("fork");
    9. }
    10. else if (id == 0)
    11. {
    12. int cnt = 5;
    13. while (cnt)
    14. {
    15. std::cout << "I am child, pid: " << getpid() << " ppid: " << getppid() << " cnt: " << cnt << std::endl;
    16. cnt--;
    17. sleep(1);
    18. }
    19. exit(0);
    20. }
    21. else
    22. {
    23. while (1)
    24. {
    25. std::cout << "I am parent, pid: " << getpid() << " ppid: " << getppid() << std::endl;
    26. sleep(1);
    27. }
    28. }
    29. return 0;
    30. }

    写了这样一个程序,可以看一下程序的前五秒情况和后面的情况。

    前五秒,子进程正常,之后子进程进入僵尸状态。


    在写代码的时候,可能会出现让父进程等待子进程或者是让父进程通过一些办法来回收僵尸进程,可以通过wait和waitpid来解决这样的问题.

    在linux中输入 man 2 wait可以查看

    1. #include
    2. #include
    3. pid_t wait(int*status);
    4. 返回值:
    5. 成功返回被等待进程pid,失败返回-1
    6. 参数:
    7. 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
    8. 调用了wait之后,就会阻塞自己,wait会自动的去分析子进程。如果找到了某个子进程
    9. 变成了僵尸进程,wait就会把它消灭。当然如果没有找到,就会一直等待。

    1. #include <iostream>
    2. #include <unistd.h>
    3. #include <sys/wait.h>
    4. #include <sys/types.h>
    5. int main()
    6. {
    7. pid_t id = fork();
    8. if (id < 0)
    9. {
    10. perror("fork");
    11. }
    12. else if (id == 0)
    13. {
    14. int cnt = 5;
    15. while (cnt)
    16. {
    17. std::cout << "I am child, pid: " << getpid() << " ppid: " << getppid() << " cnt: " << cnt << std::endl;
    18. cnt--;
    19. sleep(1);
    20. }
    21. exit(0);
    22. }
    23. else
    24. {
    25. int cnt = 10;
    26. while (cnt)
    27. {
    28. std::cout << "I am parent, pid: " << getpid() << " ppid: " << getppid() << std::endl;
    29. cnt--;
    30. sleep(1);
    31. }
    32. // 等待任意子进程退出。
    33. pid_t ret = wait(NULL);
    34. if (ret == id)
    35. {
    36. std::cout << "wait success, ret: " << ret << std::endl;
    37. }
    38. sleep(5);
    39. }
    40. return 0;
    41. }

    对上面的代码进行了一个修改。

    修改后的代码把僵尸进程给回收了。


    当然,上面的代码是针对任意子进程。

    现在创建多个子进程

    1. #include <iostream>
    2. #include <sys/types.h>
    3. #include <sys/wait.h>
    4. #include <unistd.h>
    5. #define N 10
    6. void RunChild()
    7. {
    8. int cnt = 5;
    9. while(cnt)
    10. {
    11. printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
    12. sleep(1);
    13. cnt--;
    14. }
    15. }
    16. int main()
    17. {
    18. int i;
    19. for (i = 0; i < N; i++)
    20. {
    21. pid_t id = fork();
    22. if (id == 0)
    23. {
    24. RunChild();
    25. exit(0);
    26. }
    27. std::cout << "create child process: " << id << " success " << std::endl;
    28. }
    29. sleep(10);
    30. for (i = 0; i < N ; i++)
    31. {
    32. pid_t id = wait(NULL);
    33. if (id > 0)
    34. {
    35. std::cout << "wait " << id << " success " << std::endl;
    36. }
    37. }
    38. return 0;
    39. }

    创建了十个子进程,就等待十个子进程---循环等待。


    如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不反回,默认叫做阻塞状态。

    1. pid_ t waitpid(pid_t pid, int *status, int options);
    2. 返回值:
    3. 当正常返回的时候waitpid返回收集到的子进程的进程ID;
    4. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    5. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
    6. 参数:
    7. pid:
    8. Pid=-1,等待任一个子进程。与wait等效。
    9. Pid>0.等待其进程ID与pid相等的子进程。
    10. status:
    11. WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    12. WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    13. options:
    14. WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
    15. 程的ID

    获取子进程的退出信息,是通过wait/waitpid中的statue参数来获得的。

    1. int status = 0;
    2. //pid_t ret = wait(NULL);
    3. //pid_t ret = waitpid(id, &status, 0);

    如果说,父进程在等待子进程的时候,关心子进程,可以自己定义一个status变量,当我们调用的时候,操作系统会自动的把退出信息拷贝到status变量中。这样就能拿到子进程的退出信息。

    1. #include <stdio.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <sys/types.h>
    5. #include <sys/wait.h>
    6. #include <iostream>
    7. #define N 10
    8. void RunChild()
    9. {
    10. int cnt = 5;
    11. while (cnt)
    12. {
    13. printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
    14. sleep(1);
    15. cnt--;
    16. }
    17. }
    18. int main()
    19. {
    20. pid_t id = fork();
    21. if (id < 0) {
    22. perror("fork");
    23. return 1;
    24. }
    25. else if (id == 0)
    26. {
    27. //int *p = NULL;
    28. // child
    29. int cnt = 1;
    30. while (cnt)
    31. {
    32. printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    33. cnt--;
    34. sleep(1);
    35. //*p = 100;
    36. }
    37. exit(1);
    38. }
    39. int cnt = 5;
    40. while (cnt)
    41. {
    42. printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    43. cnt--;
    44. sleep(1);
    45. }
    46. int status = 0;
    47. pid_t ret = waitpid(id, &status, 0);
    48. if (ret == id)
    49. {
    50. std::cout << "wait success , ret: " << ret << " status: " << status << std::endl;
    51. }
    52. sleep(5);
    53. return 0;
    54. }

    在输出结果之后,发现,status是 256,并不是子进程的退出码1 。


    子进程在退出的时候一共有三种场景:代码运行完毕,结果正确/不正确,代码异常终止。

    父进程等待是想要获得子进程的哪些信息呢?1.子进程代码是否异常 2.没有异常,结果对吗?不对是因为什么,不同的退出码,表示不同的出错原因。

    这个status不单单是一个int,它要被划分为多个部分。

    int 有32位,我们只考虑它的低16位,其中低七位表示的是进程是否收到信号,次低8位表示退出码。这两个字段组合,就能把所有的情况包含。

    通过kill -l可以查看信号,一共有64个信号,所以我们可以通过终止信号来判断代码是否执行完毕,结果对不对就可以通过退出状态来判断结果对不对。


    1. // 7F: 0111 1111
    2. // printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);

    添上上面的代码,就可以正确输出退出码了。


    下面我们可以自己故意设定一些错误。

    1. #include <stdio.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <sys/types.h>
    5. #include <sys/wait.h>
    6. #define N 10
    7. void RunChild()
    8. {
    9. int cnt = 5;
    10. while (cnt)
    11. {
    12. printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
    13. sleep(1);
    14. cnt--;
    15. }
    16. }
    17. int main()
    18. {
    19. pid_t id = fork();
    20. if (id < 0) {
    21. perror("fork");
    22. return 1;
    23. }
    24. else if (id == 0)
    25. {
    26. //int *p = NULL;
    27. // child
    28. int cnt = 1;
    29. while (cnt)
    30. {
    31. printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    32. cnt--;
    33. sleep(1);
    34. //*p = 100;
    35. }
    36. int a = 10;
    37. a /= 0;
    38. exit(1);
    39. }
    40. int cnt = 5;
    41. while (cnt)
    42. {
    43. printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    44. cnt--;
    45. sleep(1);
    46. }
    47. int status = 0;
    48. pid_t ret = waitpid(id, &status, 0);
    49. if (ret == id)
    50. {
    51. printf("wait success, ret: %d, exit sig: %d, exit code: %d \n", ret, status&0x7F, (status>>8)&0xFF);
    52. sleep(5);
    53. }
    54. return 0;
    55. }

    退出码就是8,在通过kill -l查看一下


    子进程在退出的时候,会把退出信息保存在子进程PCB当中,对应的父进程就会去waitpid中读取子进程PCB的退出信息,如果检测到Z状态,就读取exit_code和exit _signal合成status返回。


    非阻塞轮询

    比如说快该考试了,小张让小王去帮他复习一下操作系统这门课,小王说行啊,但现在不行,你得等会。然后小张就在楼下等着,等了三四分钟,小王没下来,打电话小王还在干其他事,陆陆续续打了十几个电话,小王终于下来了。你们两个相互看到了。

    小王就是操作系统,每一次打电话就是检查的过程。在小王有其他事情的时候,你立马挂了电话,就是非阻塞,陆陆续续打电话的过程就是轮询。

    停了几天,要考数据结构,小张还是找的小王复习,小张给小王打电话让帮它复习,小王答应了,准备挂电话的时候,小张说,电话别挂,见面了在挂电话。小王就是操作系统,小张就是用户,这种调用方式就是阻塞式调用。

    在考试的时候,你还是找小王复习,你带了一本书和手机,在你等待小王的过程中,玩玩手机,看看书。听一段时间给小王打个电话问问什么时候下来。这就是非阻塞轮询 + 自己的事情。

    1. #include <stdio.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <sys/types.h>
    5. #include <sys/wait.h>
    6. #define N 10
    7. void RunChild()
    8. {
    9. int cnt = 5;
    10. while (cnt)
    11. {
    12. printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
    13. sleep(1);
    14. cnt--;
    15. }
    16. }
    17. int main()
    18. {
    19. pid_t id = fork();
    20. if (id < 0) {
    21. perror("fork");
    22. return 1;
    23. }
    24. else if (id == 0)
    25. {
    26. //int *p = NULL;
    27. // child
    28. int cnt = 1;
    29. while (cnt)
    30. {
    31. printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
    32. cnt--;
    33. sleep(1);
    34. //*p = 100;
    35. }
    36. exit(11);
    37. }
    38. else
    39. {
    40. int status = 0;
    41. while (1) { //轮询
    42. pid_t ret = waitpid(id, &status, WNOHANG); //非阻塞
    43. if (ret > 0)
    44. {
    45. if (WIFEXITED(status))
    46. {
    47. printf("进程是正常跑完的, 退出码:%d\n", WEXITSTATUS(status));
    48. }
    49. else {
    50. printf("进程出异常了\n");
    51. }
    52. break;
    53. }
    54. else if (ret < 0)
    55. {
    56. printf("wait failed!\n");
    57. break;
    58. }
    59. else
    60. {
    61. printf("你好了没?子进程还没有退出,我在等等...\n");
    62. sleep(1);
    63. }
    64. }
    65. }
    66. return 12;
    67. }

    这就是一个非阻塞轮询。

    进程程序替换

    替换原理

    用fork创建子进程后执行的是和父进程相同的程序,但有可能执行不同的代码分支,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程所以调用exec前后该进程的id并未改变。

    通过 `man 3 exec`可以查看exec函数。


    单进程程序替换

    像这种后面带三个点的是可变参数,可以传递任意数量的参数,但最后一个参数要是NULL;

    1. #include
    2. #include
    3. #include
    4. int main()
    5. {
    6. printf("before: I an a process, pid: %d, ppod: %d\n", getpid(), getppid());
    7. execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 也可以换成其他指令,第一个参数是路径
    8. printf("after: I an a process, pid: %d, ppod: %d\n", getpid(), getppid());
    9. return 0;
    10. }

    这个代码在运行之后没有输出after。

    前面说过,进程在创建之后,会有自己的PCB,进程地址空间,页表,物理内存。通过./运行这个程序的时候,会把数据加载到物理内存中,然后进程地址空间通过页表形成一个映射关系。在执行的过程中遇到了ls这个命令,这个时候,系统会做一件暴力的事情,直接拿ls的代码替换我自己的代码,用新的可执行程序替换旧的可执行程序,然后重新开始执行。这个过程就是 程序替换。


    多进程程序替换

    1. #include <stdio.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <sys/types.h>
    5. #include <sys/types.h>
    6. int main()
    7. {
    8. pid_t id = fork();
    9. if (id == 0)
    10. {
    11. // 子进程
    12. printf("before: I an a process, pid: %d, ppod: %d\n", getpid(), getppid());
    13. execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
    14. printf("after: I an a process, pid: %d, ppod: %d\n", getpid(), getppid());
    15. exit(0);
    16. }
    17. pid_t ret = waitpid(id, NULL, 0);
    18. if (ret > 0) printf("wait success, father mypid: %d, ret id: %d\n", getpid(), ret);
    19. return 0;
    20. }

    子进程执行到exec的时候,会不会影响到父进程呢?

    不会,因为有写时拷贝技术,以及进程之间数据相互独立的特性。


    补充

    不管单进程程序替换还是多进程程序替换,相应进程中的after都没有打印出来。

    after位于execl之后,程序就被替换了,替换之后,就会重新执行新代码,也就是替换之后的代码,after已经被替换了,所以不会被执行,也不能被执行。

    1. 现象: 程序替换成功之后,exec* 后续的代码不会被执行。失败之后,才可能执行后续代码。exec* 函数,只有失败返回值,没有成功返回值。
    2. 小知识:Linux中形成的可执行程序,是有格式的,ELF,可执行程序的表头,可执行程序的入口地址。

    exec系列的程序替换函数一共有七个。‘

    execl 可以看作是 exec list,参数是一个一个的传递给函数的,名字中带l的就是一个一个的传,可变参数的。

    你要执行一个程序的第一件事情是什么---先找到这个程序。所以函数的参数第一个都是文件的全路径或者相对路径。找到这个程序之后,执行这个程序就是: 命令行怎么写,你就怎么传。

    execlp 可以看作是 execl PATH, execlp自己会在默认的PATH环境变量中直接查找。只需要写文件名字,系统会自动地去寻找文件。第一个参数是要找到的文件,后面的参数是要执行的指令。

    execv 可以看作是 exec vector,execv的第一个参数是路径,第二个参数是字符串指针数组。其实就是当我们在命令行输入ls -a -l变成了 一个指针数组的形式,必须以NULL结尾。

    在单进程程序替换中,把新旧代码覆盖并加载到内存当中,exec函数承载的是一个加载器的效果。execv就是加载器,代码级别的加载器。

    有了前面的基础,execvp是什么意思就好理解了。

    前面跳了一个execle现在来说, execl e,e可以看作是env,环境变量,我们在执行可执行程序的时候,可以传递我们自己的环境变量。

    编写了一个hello.cpp的文件,在proc.c中去调用这个程序。

    1. #include <iostream>
    2. int main(int argc, char *argv[])
    3. {
    4. for (int i = 0; argv[i]; i++)
    5. {
    6. std::cout << argv[i] << " ";
    7. }
    8. std::cout << std::endl;
    9. std::cout << "Hello World" << std::endl;
    10. std::cout << "Hello World" << std::endl;
    11. std::cout << "Hello World" << std::endl;
    12. std::cout << "Hello World" << std::endl;
    13. return 0;
    14. }
    15. #include <stdio.h>
    16. #include <unistd.h>
    17. #include <stdlib.h>
    18. #include <sys/types.h>
    19. #include <sys/types.h>
    20. int main()
    21. {
    22. pid_t id = fork();
    23. if (id == 0)
    24. {
    25. char *const myargv[] = {
    26. "hello",
    27. "-a",
    28. "-b",
    29. NULL
    30. };
    31. // 子进程
    32. printf("before: I an a process, pid: %d, ppod: %d\n", getpid(), getppid());
    33. execv("./hello",myargv);
    34. printf("after: I an a process, pid: %d, ppod: %d\n", getpid(), getppid());
    35. exit(0);
    36. }
    37. pid_t ret = waitpid(id, NULL, 0);
    38. if (ret > 0) printf("wait success, father mypid: %d, ret id: %d\n", getpid(), ret);
    39. return 0;
    40. }

    1. 先看一下makefile 文件的编写
    2. .PHONY:all
    3. all:proc hello
    4. proc:proc.c
    5. gcc -o $@ $^ -std=c99
    6. hello:hello.cpp
    7. g++ -o $@ $^
    8. .PHONY:clean
    9. clean:
    10. rm -f proc hello
    11. 这样可以一次性的编译两个文件。

    确实可以在proc中拿到信息。

    现在在hello文件中添加上main函数的第三个参数,proc文件中代码不变。

    环境变量是自动传递过去的。

    环境变量是什么时候传递过去的呢??

    首先,环境变量也是数据,当我么你创建子进程的时候,环境变量就已经被子进程继承下去,即便没有命令行参数,也可以拿到。

    所以程序替换的时候,环境变量信息不会被替换。所以想给子进程传递环境变量,应该怎么传递?

    1. 新增环境变量
    2. 彻底替换

    可以直接在父进程的地址空间中直接putenv

    在这里导入之后,跟系统中的环境变量没关系,不会出现在系统中。

    说了这么多,就是为了介绍execle的前置知识

    也可以传递自定义环境变量。

    当我们传递我们自定义的环境变量的时候,采用的策略是覆盖,而不是追加。


    命名理解

    命名理解
    这些函数原型看起来很容易混,但只要掌握了规律就很好记。
    l(list) : 表示参数采用列表
    v(vector) : 参数用数组
    p(path) : 有p自动搜索环境变量PATH
    e(env) : 表示自己维护环境变量
    事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在
    man手册第3节。这些函数之间的关系如下图所示


    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <assert.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <sys/wait.h>
    8. #include <fcntl.h>
    9. #define LEFT "["
    10. #define RIGHT "]"
    11. #define LABLE "#"
    12. #define LINE_SIZE 1024
    13. #define ARGC_SIZE 32
    14. #define DELIM " "
    15. #define EXIT_CODE 44
    16. char myenv[LINE_SIZE];
    17. int last_code = 0;
    18. int quit = 0;
    19. char pwd[LINE_SIZE];
    20. extern char** environ;
    21. const char* getusername()
    22. {
    23. return getenv("USER");
    24. }
    25. const char* gethostname()
    26. {
    27. return getenv("HOSTNAME");
    28. }
    29. void getpwd()
    30. {
    31. getcwd(pwd,sizeof pwd);
    32. }
    33. void Interact(char *cline, int size)
    34. {
    35. getpwd();
    36. printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
    37. char *s = fgets(cline, size, stdin);
    38. assert(s != NULL);
    39. (void)s;
    40. cline[strlen(cline) - 1] = '\0';
    41. }
    42. int splistring(char* commandline,char *argv[])
    43. {
    44. int i = 0;
    45. argv[i++] = strtok(commandline, DELIM);
    46. while (argv[i++] = strtok(NULL, DELIM));
    47. return i - 1;
    48. }
    49. int buildCommand(char *_argv[], int _argc)
    50. {
    51. if (_argc == 2 && strcmp(_argv[0], "cd") == 0)
    52. {
    53. chdir(_argv[1]);
    54. getpwd();
    55. sprintf(getenv("PWD"), "PWD=%s", pwd);
    56. return 1;
    57. }
    58. else if (strcmp(_argv[0], "ls") == 0)
    59. {
    60. _argv[_argc++] = "--color";
    61. _argv[_argc] = NULL;
    62. }
    63. else if (_argc == 2 && strcmp(_argv[0], "export") == 0)
    64. {
    65. if (strcmp(_argv[1], "$?") == 0)
    66. {
    67. printf("%d\n", last_code);
    68. last_code = 0;
    69. }
    70. strcpy(myenv, _argv[1]);
    71. putenv(myenv);
    72. return 1;
    73. }
    74. else if (_argc == 2 && strcmp(_argv[0], "echo") == 0)
    75. {
    76. if (*_argv[1] == '$')
    77. {
    78. char *val = getenv(_argv[1] + 1);
    79. if (val) printf("%s\n", val);
    80. }
    81. else
    82. {
    83. printf("%s\n", _argv[1]);
    84. }
    85. return 1;
    86. }
    87. return 0;
    88. }
    89. void NormalExcute(char *_argv[])
    90. {
    91. pid_t id = fork();
    92. if (id < 0)
    93. {
    94. perror("fork error");
    95. return ;
    96. }
    97. else if (id == 0)
    98. {
    99. execvpe(_argv[0], _argv, environ);
    100. exit(EXIT_CODE);
    101. }
    102. else
    103. {
    104. int status = 0;
    105. pid_t rid = waitpid(id, &status, 0);
    106. if (rid == id)
    107. {
    108. last_code = WEXITSTATUS(status);
    109. }
    110. }
    111. }
    112. int main()
    113. {
    114. char commandline[LINE_SIZE];
    115. char *argv[ARGC_SIZE];
    116. while (!quit)
    117. {
    118. Interact(commandline, sizeof (commandline));
    119. int argc = splistring(commandline, argv);
    120. if (argc == 0)
    121. {
    122. continue;
    123. }
    124. int n = buildCommand(argv, argc);
    125. if (!n) NormalExcute(argv);
    126. // for (int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
    127. // printf("echo : %s\n", commandline);
    128. }
    129. return 0;
    130. }

  • 相关阅读:
    7年测试工程师分享的20K的测试“卷王真经”
    奇点云:企业级数据基础设施的设计思路是“操作系统”
    HTML5编写旅游网页
    Maven详解
    微服务学习第三十二节
    jmeter 聚合报告
    Ruby 环境变量
    有哪些电容笔值得推荐?值得买的电容笔测评
    OpenCV 基础图像处理
    Java面试题火了:这可能是历史上最简单的一道面试题了
  • 原文地址:https://blog.csdn.net/weixin_73888239/article/details/133753967