Linux进程基础(一)

操作系统

在学Linux之前,首先我们先了解一下Linux操作系统,内核,以及他内核态和用户态等等的概念

概念:操作系统是管理计算机硬件与软件资源的计算机程序,简称OS

为什么要有操作系统

1.给用户提供稳定、高效和安全的运行环境,为程序员提供各种基本功能(OS不信任任何用户,不让用户或者程序员直接与硬件进行交互)。

2.管理好各种软硬件资源。

从图中可以看到OS是软件和硬件资源的管理者,用户不能直接操作系统软件和硬件部分,只能通过操作系统提供的接口间接操作,OS是跑在硬件之上的,操作系统运行之后,用户程序才可以使用,用户程序运行在操作系统之上,OS拥有绝对权力。

shell初识

Linux严格意义上说的是一个操作系统,我们称之为“核心(kernel)“ ,但我们一般用户,不能直接使用kernel。 而是通过kernel的“”程序,也就是所谓的shell,来与kernel沟通。

shell:实际上是一个命令的解释程序,是一个用户与系统内核沟通的桥梁。

内核:完成系统核心功能,对软硬件资源进行控制管理的系统集合—用户不能直接操作。

对比windows GUI,我们操作windows 不是直接操作windows内核,而是通过图形接口,点击,从而完成我们的 操作(比如进入D盘的操作,我们通常是双击D盘盘符.或者运行起来一个应用程序)。
shell 对于Linux,有相同的作用,主要是对我们的指令进行解析,解析指令给Linux内核。反馈结果在通过内核运 行出结果,通过shell解析给用户。

总结:

shell是进行命令行解释的 ,命令行解释器在系统层面上就是一个进程!

保证内核的安全运行的同时,也要保证自身的安全,如何保护?shell进程->创建子进程去进行命令行解释->如果失败是不会影响shell进程。

大部分指令的运行,都是会变成进程的。

内核态和用户态

对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
对上面这段内容我们可以这样理解:
每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。
换句话说就是, 最高 1G 的内核空间是被所有进程共享的!这1G的内核空间内存放的都是Linux操作系统的代码,而剩下的3G的空间存放的则是应用程序代码,比如QQ、酷我音乐等等用户应用程序

好了我们现在需要再解释一下什么是内核态、用户态:
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而用户编写的应用程序代码可以很容易的让操作系统崩溃掉。
对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行(Linux 可是个多任务系统啊!)。

所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。

进程

进程和程序的概念

我们平时所写的C语言代码,通过编译器的编译,最终会成为一个可执行的程序,当这个可执行程序运行起来之后,它就变成了一个进程。

程序是存放在存储介质(程序平时都存放在磁盘当中)上的一个可执行文件,而进程就是程序执行的过程。进程的状态是变化的,其中包括进程的创建、调度和死亡。程序是静态的,进程是动态的。

进程: 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

如何描述进程

  • 进程的所有属性信息都被放在一个叫做进程控制块的结构体中,可以理解为进程属性的集合。
  • 这个数据结构的英文名称是PCB(process control block),在Linux的OS下的PCB是task_struct(Linux内核中的一种数据结构,它会被装载到RAM(内存)中并且包含并包含进程的信息)。

task_struct内容有哪些?

  • 标识符:描述本进程的唯一标识符(就像是我们每个人的身份证)。

  • 状态:任务状态、退出代码、退出信号等。

  • 优先级: 程序被CPU执行的顺序(后面会单独介绍)。

  • 程序计数器: 一个寄存器中存放了一个pc指针,这个指针永远指向即将被执行的下一条指令的地址。

  • 内存指针: 包含程序代码和进程相关的数据的指针,还有和其它进程共享的内存快的指针。这样就可以PCB找到进程的实体。

  • 上下文数据: 在单核CPU中,进程需要在运行队列(run_queue) 中排队,等待CPU调度,每个进程在CPU中执行时间是在一个时间片内的,时间片到了,就要从CPU上下来,继续去运行队列中排队。

  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。

  • 记账信息: 能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

    组织进程
    在内核源代码中发现,所有运行在系统里的进程都以task_struct链表形式存在内核中。

进程的状态

进程的状态反应进程执行过程的变化。这些状态随着进程的执行和外界的变化而转换。

五态模型中,进程分为新建态,终止态,运行态,就绪态,就绪态。

3

(1)TASK_RUNNING(运行态):进程正在被CPU执行。当一个进程被创建的时候会处于TASK_RUNNABLE,表示已经准备就绪,正在准备被调度。

(2)TASK_INTERRUPTIBLE(可中断状态):进程正在睡眠(阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置成运行态。处于此状态的进程也会因为接收到信号而提前被唤醒,比如给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将会被先唤醒(进入TASK_RUNNABLE状态),然后再响应SIGKILL信号而退出(变为TASK_ZOMBIE状态),并不会从TASK_INTERRUPTIBLE状态直接退出。

(3)TASK_UNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源被满足的时候被唤醒,但是不可以由其他进程通过信号或者中断唤醒。由于不接受外来的任何信号,因此无法用KILL杀掉这些处于该状态的进程。而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。

(4)TASK_ZOMBIE(僵死):表示进程已经结束,但是其父进程还没有回收子进程的资源。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用wait函数释放子进程的资源,子进程的进程描述符就会被释放。

(5)TASK_STOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE状态。

下面是进程状态在源码中的定义:

static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

查看进程状态相关的命令

ps命令可以查看进程详细的状态,常用选项如下:

选项 含义
-a 显示终端上的所有进程,包括其他进程
-u 显示进程的详细状态
-x 显示没有控制终端的进程
-w 显示加宽,以便显示更多的信息
-r 只显示正在运行的进程

PID就是进程的进程号,STAT是进程此时处于什么状态。

有下面两种命令(前者查看所用进程的名字,后者可以查看进程的父子关系):

ps aux/ps axj

进程号和相关函数

每个进程都有一个进程号来标识,其类型为pid_t(整型)。进程号是唯一的,但是进程号是可以重用的。当一个进程终止后,其进程号可以再次使用。

进程号(PID)

getpid()可以获取当前进程的进程号。

父进程号(PPID)

getppid()可以获取当前进程的父进程号

进程组号(PGID)

getpgid()可以获取当前进程进程组号

进程创建

fork函数(系统调用)

pid_t fork(void);

功能:通过复制当前进程,为当前进程创建一个子进程

返回值:成功:子进程中返回0,父进程中返回子进程的pid_t。

​ 失败:返回-1。

进程调用fork函数,内核需要做什么

  • 给子进程分配内存空间,并为子进程创建PCB
  • 将父进程部分数据结构内容(还有代码和数据暂时共享)拷贝至子进程
  • 添加子进程到系统进程列表(运行队列)当中
  • fork返回,开始CPU调度器调度

fork之后执行什么?

父子进程共享一份代码,fork之后,一起执行fork之后的代码,且二者之间是独立的,不会相互影响。
父进程经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈,就像父进程把自己克隆了一遍。事实上,父进程只复制了自己的PCB块。而代码段,数据段和用户堆栈内存空间并没有复制一份,而是与子进程共享。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。
父进程绝大部门东西都被子进程继承,代码也是,但是在执行的过程中,父进程的PCB中存在一个pc指针,记录着下一条指定的地址,当父进程执行到fork的时候,pc指针也只想fork的下一条指令,子进程也继承了pc指针的虚拟地址,本来子进程全部继承了父亲的共享代码,但是此时pc也是指向fork的下一条指令,所以父子进程都从fork之后开始执行。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  pid_t ret = fork();
  
  if (ret < 0)
  {
    perror("fork");
    return 1;
  }
  else if (ret == 0)// 子进程
  {
    printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }
  else if (ret > 0)// 父进程
  {
    printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
    sleep(1);
  }

  sleep(1);

  return 0;
}

父子进程关系

使用fork函数得到的子进程是父进程的一个复制品,每个进程都有自己的进程控制块PCB,再这个PCB中子进程从父进程中继承了整个进程的地址空间:包括进程上下文,进程堆栈,打开的文件描述符,信息控制设定,进程优先级,进程组号等等,但是进程的地址空间都是虚拟空间,子进程PCB继承的都是虚拟地址。

写时拷贝

通常情况下,父子进程共享一份代码,并且数据都是共享的,当任意一方试图写入更改数据的时候,那么这一份便要以写时拷贝的方式各自私有一份副本。

从图中可以看出,发生写时拷贝后,修改方将改变页表中对该份数据的映射关系,父子进程各自私有那一份数据,且权限由只读变成了只写,虚拟地址没有改变,改变的是物理内存页的物理地址。(涉及到虚拟地址,可以看我上面发的文章)

问题思考

1.为什么代码要共享?

代码是不可以被修改的,所以各自私有很浪费空间,大多数情况下是共享的,但要注意的是,代码在特殊情况下也是会发生写时拷贝的,也就是进程的程序替换(后面会单独介绍)。

2.写实拷贝的作用?

  • 可以减少空间的浪费,在双方都不对数据或代码进行修改的情况下,各自私有一根数据和代码是浪费空间的。
  • 维护进程之间的独立性,虽然父子进程共享一份数据,但是父子中有一方对数据进行修改,那么久拷贝该份数据到给修改方,改变修改方中页表对这份数据的映射关系,然后对数据进行修改,这样不管哪一方对数据进行修改都不会影响另一方,这样就做到了独立性。

3.写时拷贝是对所有数据进行拷贝吗?

答案是否定的。如果没有修改的数据进行拷贝,那么这样还是会造成空间浪费的,没有被修改的数据还是可以共享的,我们只需要将修改的那份数据进行写时拷贝即可。

理论还是太枯燥,上代码!

代码1:栈区局部变量

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
   int var = 88;
   //创建一个子进程
   pid_t ret = fork();
   if (ret < 0)
    {
        perror("fork");
        return 1;
    }
   else if (ret == 0)// 子进程
    {
        sleep(1);
        printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("子进程睡醒之后 var = %d\n",var);
    }
        else if (ret > 0)// 父进程
   {
        printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("父进程之前 var =%d\n", var);
        var++;
        printf("父进程之后 var =%d\n", var);
   }
        sleep(1);
        return 0; 
 }

运行结果:

读时共享,写时拷贝。这里的父进程一开始时共享var的数据给子进程,但是此时子进程睡了一秒,就执行父进程,父进程中var的值被改变,此时写时拷贝,var会拷贝一份到子进程当中,所以父进程修改var的值不会影响到子进程中var的值。这里的局部变量在栈区。

代码2:全局变量

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int var = 88;
int main()
{
   //创建一个子进程
   pid_t ret = fork();
   if (ret < 0)
    {
        perror("fork");
        return 1;
    }
   else if (ret == 0)// 子进程
    {
        sleep(1);
        printf("I am child-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("子进程睡醒之后 var = %d\n",var);
    }
        else if (ret > 0)// 父进程
   {
        printf("I am parent-pid:%d, ppid:%d\n", getpid(), getppid());
        printf("父进程之前 var =%d\n", var);
        var++;
        printf("父进程之后 var =%d\n", var);
   }
        sleep(1);
        return 0; 
 }

运行结果:

子进程var值也不会受到影响,遵循读时共享,写时拷贝的原则。

总结:

  • 父子进程由独立的数据段、堆、栈、共享代码段(每个进程都有属于自己的PCB)。

  • Linux中每个进程都有4G的虚拟地址空间(独立的3G用户空间和共享的1G内核空间),fork创建的子进程也不例外。

    (1)1G内核空间既然是所有进程共享,因此fork创建的子进程自然也将有用;

    (2)3G的用户空间是从父进程而来。

  • fork创建子进程时继承了父进程的数据段、代码段、栈、堆,值得注意的是父进程继承来的是虚拟地址空间,进程上下文,打开的文件描述符,信息控制设定,进程优先级,进程组号,同时也复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟地址,映射的物理内存也是一致的。(独立的虚拟地址空间,共享父进程的物理内存)。

  • 由于父进程和子进程共享物理页面,内核将其标记为“只读”,父子双方均无法对其修改。无论父子进程尝试对共享的页面执行写操作,就产生一个错误,这时内核就把这个页复制到一个新的页面给这个进程,并把原来的只读页面标志为可写,留给另外一个进程使用----写时复制技术。

  • 内核在子进程分配物理内存的时候,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程映射的时同一块物理内存。

进程终止

可以通过echo$?查看进程退出码

exit函数和return函数的区别

  • main函数结束的时候也会隐式的调用exit函数。exit函数运行的时候首先会执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所有的输出流,关闭所有打开的流并且关闭通过标准IO函数创建的临时文件。
  • exit时结束一个进程,他将删除进程使用的内存空间,同时把错误信息返回父进程;而return是返回函数值(return所在的函数框内)并且退出函数。通常情况:exit(0)表示程序正常, exit(1)和exit(-1)表示程序异常退出,exit(2)表示表示系统找不到指定的文件。在整个程序中,只要调用exit就结束(当前进程或者在main时候为整个程序)。return也是如此,如图return在main函数中,那么结束的就是整个进程。return是函数的结束,exit是进程的结束。
  • return是语言级别的,它表示了调用堆栈的返回return( )是当前函数返回,当然如果是在主函数main, 自然也就结束当前进程了,如果不是,那就是退回上一层调用。在多个进程时。如果有时要检测上个进程是否正常退出。就要用到上个进程的返回值,依次类推。而exit是系统调用级别的,它表示了一个进程的结束
  • exit函数是退出应用程序,并将应用程序的一个状态返回给OS,这个状态标识了应用程序的一些运行信息。
  • 在main函数中exit(0)等价于return 0。

1.return函数返回退出码

main函数退出的时候,return的返回值就是进程的退出码。0在函数的设计中,一般代表是正确而非0就是错误。

2.调用exit函数

void exit(int status);

功能:结束当前正在执行的进程。

参数:返回给父进程的参数,根据需要填写。

在任意位置调用,都会使得进程退出,调用之后会执行执行用户通过 atexit或on_exit定义的清理函数,还会 关闭所有打开的流,所有的缓存数据均被写入。

int main()
{
  cout << "12345";
  sleep(3);
  exit(0);// 退出进程前前会执行用户定义的清理函数,且刷新缓冲区
  return 0;
}//输出12345

3.调用_exit函数

exit()和_exit()函数功能和用法都是一样的,但是区别就在于exit()函数是标准库函数,而__exit函数是系统调用。

在Linux的标准函数库中,有一套称做“高级I/O”的函数,我们熟知的printf(),fopen(),fread(),fwrite()都在此列,它们也被称作缓冲IO (buffered IO)",其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
  • exit()作为库函数,封装的比较完善,exit将终止调用的进程,在退出程序之前,所有文件关闭,缓冲区刷新(输出内容),将刷新定义,并且调用所有已刷新的“出口函数”,在执行完清理工作之后,会调用_exit来终止进程。
  • _exit()调用,但是不关闭文件,不刷新缓冲区,也不调用出口函数。
int main()
{
  cout << "12345";
  sleep(3);
   _exit(0);// 直接退出进程,不刷新缓冲区
  return 0;
}//不输出12345

4.异常终止

  • ctrl+C终止前台进程
  • kill发生9号信号杀死进程

进程等待

进程等待的必要性:

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等,这就是在执行exit时候执行的工作。但是仍然会保留一定的信息,这些信息主要指的是进程控制块PCB的信息(包括进程号,退出状态,运行事件等),而这些信息需要父进程调用wait或者waitpid函数得到他的退出状态同时彻底清理掉这个进程残留的信息。

  • 子进程必须要比父进程先退出,否则会变成孤儿孤儿进程
  • 父进程必须读取子进程的退出状态,回收子进程的资源。如果父进程不读取子进程退出状态,还不会释放子进程资源,那么子进程将处于僵死状态,会造成内存泄漏
  • 父进程派给子进程的任务完成的如何,得知子进程执行结果

wait方法

*pid_wait(int status);

功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。

参数:status进程退出时候的状态。

返回值:成功:返回结束子进程的进程号。失败:-1.

注意以下几点:

  • 调用wait会阻塞当前的进程,直到任意一个子进程退出或者收到一个不能忽视的信号才能被唤醒。
  • 若调用进程没有子进程,该函数立刻返回;若它的子进程已经结束,该函数同样会立刻返回,并且会回收那个早已经结束进程的资源。
  • 如果参数status的值不是NULL,wait就会把子进程退出时候的状态取出来并存入,这是一个整数值,指出了子进程是正常退出还是被非正常结束。

演示:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
	pid_t ret= fork();
	if (ret< 0){
	  cerr << "fork error" << endl;
	}
	else if (ret== 0){
	  // child
	  int count = 5;
	  while (count){
		printf("child[%d]:I am running... count:%d\n", getpid(), count--);
		sleep(1);
	  }
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	sleep(10);
	pid_t id = wait(NULL);// 不关心子进程退出状态
	
	printf("father finish waiting...\n");
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
	//父进程再活5秒 
	sleep(5);
	return 0;
}

由运行结果可以看出,父进程一只等待子进程结束,等待的时候子进程变成僵尸进程,等父进程彻底释放资源,子进程的状态由僵尸变成死亡状态。

waitpid方法

*pid_t waitpid(pid_t pid, int status , int options);

功能:等待子进程结束,如果子进程终止,此函数就会回收子进程资源。

参数

pid:参数pid有以下几种类型:

​ pid>0 等待进程ID等于pid的子进程结束。

​ pid=0 等待同一个进程组中的任何子进程,如果子进程已经进入了别的进程组,waitpid不会等待它。

​ pid=-1 等待任意子进程,此时waitpid和wait的作用是一样的。

​ pid<-1 等待指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

options:options提供了一些额外的选项来控制waitpid()

​ 0:通wait(),阻塞父进程,等待子进程退出。

​ WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID(可以进行基于阻塞等待的轮询访问)。

​ WUNTRACED:如果子进程暂停了此函数立马返回,并且不予理会子进程的结束状态(很少调用)。

返回值:

​ waitpid有三种情况:

(1)正常返回的时候,waitpid返回收集到的已回收子进程的进程的进程号。

(2)如果设置了WNOHANG,而调用中发现了没有已经退出的子进程可以等待,返回0。

(3)如果调用中出错,返回-1,此时errno会被设置成相应的值来指示错误所在。

代码示例:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
	pid_t ret= fork();
	if (ret< 0){
	  cerr << "fork error" << endl;
	}
	else if (ret== 0){
	  // child
	  int count = 5;
	  while (count){
		printf("child[%d]:I am running... count:%d\n", getpid(), count--);
		sleep(1);
	  }
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	sleep(10);
	pid_t id = waitpid(-1, NULL, 0);// 不关心子进程退出状态,以阻塞方式等待
	
	printf("father finish waiting...\n");
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
	//父进程再活5秒 
	sleep(5);
	return 0;
}

获取子进程的status

  • wait和waitpid中都有一个status参数,该参数是一个输出型参数,由操作系统来填充
  • 如果该参数给NULL,那么代表不关心子进程的退出信息

status的几种状态:(我们只研究status的低16位)

看图可以知道,低7位代表的是终止信号,第8位时core dump标志,高八位是进程退出码(只有正常退出是这个退出码才有意义)
status的0-6位和8-15位有不同的意义。我们要先读取低7位的内容,如果是0,说明进程正常退出,那就获取高8位的内容,也就是进程退出码;如果不是0,那就说明进程是异常退出,此时不需要获取高八位的内容,此时的退出码是没有意义的。

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
	pid_t ret = fork();
	if (ret < 0){
	  cerr << "fork error" << endl;
	}
	else if (ret == 0){
	  // child
	  int count = 5;
	  while (count){
	    printf("child[%d]:I am running... count:%d\n", getpid(), count--);
	    sleep(1);
	  }
	
	  exit(1);
	}
	// parent
	printf("father begins waiting...\n");
	
	int status;
	pid_t id = wait(&status);// 从status中获取子进程退出的状态信息
	printf("father finish waiting...\n");
	
	if (id > 0 && (status&0x7f) == 0){
	  // 正常退出
	  printf("child success exited, exit code is:%d\n", (status>>8)&0xff);
	}
	else if (id > 0){
	  // 异常退出
	  printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
	}
	else{
	  printf("father wait failed\n");
	}
	if (id > 0){ 
	  printf("child success exited\n"); 
	} else{
	  printf("child exit failed\n"); 
	} 
 	return 0;
}

运行结果如下:

阻塞等待和非阻塞等待

操控者: 操作系统
阻塞的本质: 父进程从运行队列放入到了等待队列,也就是把父进程的PCB由R状态变成S状态,这段时间不可被CPU调度器调度
等待结束的本质: 父进程从等待队列放入到了运行队列,也就是把父进程的PCB由S状态变成R状态,可以由CPU调度器调度

阻塞等待: 父进程一直等待子进程退出,期间不干任何事情

示例:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
  pid_t id = fork();
  if (id < 0){
    cerr << "fork error" << endl;
  }
  else if (id == 0){
    // child
    int count = 5;
    while (count){
      printf("child[%d]:I am running... count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(0);
  }
  
  // 阻塞等待
  // parent
  printf("father begins waiting...\n");
  int status;
  pid_t ret = waitpid(id, &status, 0);
  printf("father finish waiting...\n");

  if (id > 0 && WIFEXITED(status)){
    // 正常退出
    printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
  }
  else if (id > 0){
    // 异常退出
    printf("child exit failed,core dump is:%d,exit singal is:%d\n", (status&(1<<7)), status&0x7f);
  }
  else{
    printf("father wait failed\n");
  }
}

运行结果如下:

14

非阻塞等待: 父进程不断检测子进程的退出状态,期间会干其他事情(基于阻塞的轮询等待)

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
  pid_t id = fork();
  if (id < 0){
    cerr << "fork error" << endl;
  }
  else if (id == 0){
    // child
    int count = 5;
    while (count){
      printf("child[%d]:I am running... count:%d\n", getpid(), count--);
      sleep(1);
    }
    exit(0);
  }
  // 基于阻塞的轮询等待
  // parent
  while (1){
    int status;
    pid_t ret = waitpid(-1, &status, WNOHANG);
    if (ret == 0){
      // 子进程还未结束
      printf("father is running...\n");
      sleep(1);
    }
    else if (ret > 0){
      // 子进程退出
      if (WIFEXITED(status)){
        // 正常退出
        printf("child success exited, exit code is:%d\n", WEXITSTATUS(status));
      }
      else{
        // 异常退出
        printf("child exited error,exit singal is:%d", status&0x7f);
      }
      break;
    }
    else{
      printf("wait child failed\n");
      break;
    }
  }
  
}

运行结果如下:

posted @ 2022-10-22 11:47  一只少年AAA  阅读(425)  评论(2编辑  收藏  举报