进程内存机制及系统调用机制

一、进程概念

​ 一个程序文件(program),只是一堆待执行的代码和部分待处理的数据,他们只有被加载到内存中,然后让 CPU 逐条执行其代码,根据代码做出相应的动作,才形成一个真正“活的”、动态的进程(process),因此进程是一个动态变化的过程,是一出有始有终的戏,而程序文件只是这一系列动作的原始蓝本,是一个静态的剧本。以下图更好地展示了程序和进程的关系:
进程内存机制及系统调用机制_第1张图片

上图中的程序文件,是一个静态的存储于外部存储器(比如磁盘、flash 等掉电非易失器件)之中的文件,里面包含了将来进程要运行的“剧本”,即图中看到的执行时会被拷贝到内存的数据和代码。除了除了这些部分,ELF 格式中的大部分数据跟程序本身的逻辑没有关系,只是程序被加载到内存中执行时系统需要处理的额外的辅助信息。另外注意到.bss 段,这里面放的是未初始化的静态数据,他们是不需要被拷贝的。

当这个 ELF 格式的程序被执行时,内核中实际上产生了一个叫 task_struct{} 的结构体来表示这个进程。进程是一个“活动的实体”,这个活动的实体从一开始诞生就需要各种各样的资源以便于生存下去,比如内存资源、CPU 资源、文件、信号、各种锁资源等,所有这些东西都是动态变化的,这些信息都被事无巨细地一一记录在结构体 task_struct 之中,所以这个结构体也常常被成为进程控制块(PCB,即 Process Control Block)。

ELF到进程是一个程序被OS加载到RAM中执行后的一个完整的执行环境。

二、进程的内存布局

Linux 操作系统为了更好更高效地使用内存,将实际物理内存进行了映射,对应用程序屏蔽了物理内存的具体细节,将一块物理内存映射为应用层当中的虚拟内存,这样有利于简化程序的编写和系统统一的管理。
进程内存机制及系统调用机制_第2张图片

从上图可以看到,一个用户进程可以访问的内存区域介于 0x0804 8000 和 0xc0000000 之间,这个“广袤”的区域又被分成了几个部分,分别用来存放进程的代码和数据,以及进程在运行时产生的动态信息。下面从上往下一个个来剖析

1. 栈内存

​ 栈内存(以下简称栈)指的是从 0xC000 0000 往下增长的这部分内存区域,之所以被称为“栈”是因为进程在使用这块内存的时候是严格按照“后进先出”的原则来操作的,而这种后进先出的逻辑,就被称为栈。

​ 栈的全称是“运行时栈(run-time stack)”,顾名思义栈会随着进程的运行而不断发生变化:一旦有新的函数被调用,就会立即在栈顶分配一帧内存,专门用于存放该函数内定义的局部变量(包括所有的形参),当一个函数执行完毕返回之后,他所占用的那帧内存将被立即释放,在上图中用一根虚线和箭头来表示栈的这种动态特征。

栈主要就是用来存储进程执行过程中所产生的局部变量的,当然为了可以实现函数的嵌套调用和返回,栈还必须包含函数切换时当下的代码地址和相关寄存器的值,这个过程被称为“保存现场”,等被调函数执行结束之后,再“恢复现场”。因此,如果进程嵌套调用了很多函数,就会导致栈不断增长,但是栈的大小又是有一个最大限度的,这个限度一般是8MB,超过了这个最大值将会产生所谓的“栈溢出”导致程序崩溃,所以我们在进程中不宜嵌套调用太深的函数,也不要定义太多太大的局部变量。

2. 堆内存

​ 堆内存(以下简称堆)是一块自由内存,原因是在这个区域定义和释放变量完全由你来决定,即所谓的自由区。堆跟栈的最大区别在于堆是不设大小限制的,最大值取决于系统的物理内存。
​ 堆的全称是“运行时堆(run-time heap)”,跟栈一样,会随着进程的运行而不断地增大或缩小,由于对堆的操作非常重要,因为在此区域定义的内存的生命周期我们是可以控制的,对比其他区域的内存则不然,比如栈内存,栈的特点就是临时分配临时释放,一个变量如果是局部变量,他就会被定义在栈内存中,一旦这个局部变量所在的函数退出,不管你愿不愿意该局部变量也就会被立即释放,再如静态数据,他们都被存储在数据段,如前所述,这些变量将一直占用内存直到进程退出为止。堆内存的生命周期是:从 malloc( )/calloc( )/realloc( ) 始,到 free( )` 结束,其分配和释放完全由我们开发者自定义,这就给了我们最大的自由灵活性,让程序在运行的过程当中,以最大的效益使用内存。

堆内存操作 API 的介绍如下:

功能:在堆中申请一块大小为 size 的连续的内存
头文件: #include <stdlib.h>
    
原型:void *malloc(size_t size);
参数:
    size:对内存大小(字节)
返回值:
	成功 新申请的内存基地址
	失败 NULL
备注:
    该函数申请的内存是未初始化的
功能:在堆中申请一个具有 n 个元素的匿名数组,每个元素大小为 size
    
原型:void *calloc(size_t n, size_t size);
返回值:
	成功 新申请的内存基地址
	失败 NULL
备注:
	该函数申请的内存将被初始化为 0
功能:将 ptr 所指向的堆内存大小扩展为 size
    
原型:void *realloc(void *ptr, size_t size);
返回值:
	成功 扩展后的内存的基地址
	失败 NULL
备注:
	1,返回的基地址可能跟原地址 ptr 相同,也可能不同(即发生了迁移)
	2,当 size 为 0 时,该函数相当于相当于 free(ptr);
功能:将指针 ptr 所指向的堆内存释放
    
原型:void free(void *ptr);
返回值:
	无
备注:
	参数 ptr 必须是 malloc( )/calloc( )/realloc( )的返回值

以上几个堆内存操作函数的使用是很简单的,最后要额外说明一下的是函数 free§,他的作用是释放 p 所指向的堆内存,但是并不会改变 p 本身的值,也就是说释放了之后 p就变成了一个野指针了,下次要引用指针 p 必须对他重新赋值.

3. 数据段

数据段实际上分为三部分,地址从高到底分别是.bss 段、.data 段和.rodata 段,三个数据段各司其职:.bss 专门用来存放未初始化的静态数据,它们都将被初始化为 0,.data段专门存放已经初始化的静态数据,这么初始值从程序文件中拷贝而来,而.rodata 段用来存放只读数据,即常量,比如进程中所有的字符串、字符常量、整型浮点型常量等。

4. 代码段

代码段实际上也至少分为两部分:.text 段和.init 段。.text 段用来存放用户程序代码,也就是包括 main 函数在内的所有用户自定义函数,而 .init 段则用来存储系统给每一个可执行程序自动添加的“初始化”代码,这部分代码功能包括环境变量的准备、命令行参数的组织和传递等,并且这部分数据被放置在了栈底

5. 总结说明

以下以程序中在内存中的具体分布来具体说明内存区域的情况
进程内存机制及系统调用机制_第3张图片

  • 栈中的环境变量和命令行参数在程序一开始运行之时就被固定在了栈底(即紧挨着内核的地方),且在进程在整个运行期间不再发生变化,假如进程运行时对环境变量的个数或者值做了修改,则为了能够容纳修改后的内容,新的环境变量将会被拷贝放置到堆中。栈还有一个名称叫做“堆栈”,这是中文比较奇葩的地方:“堆栈”跟“堆”没有半毛钱关系。
  • 栈和堆都是动态变化的,分别向下和向上增长,大小随着进程的运行不断变大变小。
  • 静态数据指的是:所有的全局变量,以及 static 型局部变量
  • 数据段的大小在进程一开始运行就是固定的,其中.rodata 存放程序中所有的常量,.data 存放所有的静态数据,而如果静态数据未被初始化,则程序刚开始运行时系统将会自动将他们统统初始化为 0 然后放置在.bss 段中,这么做的原因是要节省磁盘存储空间:由于未初始化的静态数据在运行时一概会被初始化为 0,因此在程序文件中就没有必要保存任何未初始化的变量的值了。
  • 如果没有一个极具说服力的理由,我们应该尽量避免使用静态数据,因为滥用静态数据至少有两个缺点:
  • 用户代码所在的.text 段也称为正文段,.text 是一个默认的名称,他将会囊括用户定义的所有的函数代码,实际上我们可以将某些指定的函数放置到自己指定段当中去,比如在程序代码中有一段音乐数据,我们可以将此段数据放置在一个.mp3 的代码段当中,而.init 段是存放的系统初始化代码,这部分代码之所以要放置在.init 段是因为这个段当中的代码默认只会被执行一遍(初始化只能执行一遍),完成任务之后所占据的内存会被立即释放,以便节省系统资源,因此我们自己定义的函数如果也是在进程开始之初只执行一遍就不再需要,那么也可以将之放置在该段中.

三、进程操作

1. C源码变成进程的过程

进程内存机制及系统调用机制_第4张图片

2. 编译命令:

进程内存机制及系统调用机制_第5张图片
进程内存机制及系统调用机制_第6张图片
readelf 命令查看elf 文件状态

sevan@ubuntu:TCP_blocking_model$ readelf client -S
There are 31 section headers, starting at offset 0x3c20:

节头:
  [号] 名称              类型             地址              偏移量
       大小              全体大小          旗标   链接   信息   对齐
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000318  00000318
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.gnu.propert NOTE             0000000000000338  00000338
       0000000000000020  0000000000000000   A       0     0     8
  [ 3] .note.gnu.build-i NOTE             0000000000000358  00000358
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .note.ABI-tag     NOTE             000000000000037c  0000037c
       0000000000000020  0000000000000000   A       0     0     4
  [ 5] .gnu.hash         GNU_HASH         00000000000003a0  000003a0
       0000000000000028  0000000000000000   A       6     0     8
  [ 6] .dynsym           DYNSYM           00000000000003c8  000003c8
       0000000000000228  0000000000000018   A       7     1     8
  [ 7] .dynstr           STRTAB           00000000000005f0  000005f0
       0000000000000124  0000000000000000   A       0     0     1
  [ 8] .gnu.version      VERSYM           0000000000000714  00000714
       000000000000002e  0000000000000002   A       6     0     2
  [ 9] .gnu.version_r    VERNEED          0000000000000748  00000748
       0000000000000050  0000000000000000   A       7     2     8
  [10] .rela.dyn         RELA             0000000000000798  00000798
       00000000000000d8  0000000000000018   A       6     0     8
  [11] .rela.plt         RELA             0000000000000870  00000870
       0000000000000180  0000000000000018  AI       6    24     8
  [12] .init             PROGBITS         0000000000001000  00001000
       000000000000001b  0000000000000000  AX       0     0     4
  [13] .plt              PROGBITS         0000000000001020  00001020
       0000000000000110  0000000000000010  AX       0     0     16
  [14] .plt.got          PROGBITS         0000000000001130  00001130
       0000000000000010  0000000000000010  AX       0     0     16
  [15] .plt.sec          PROGBITS         0000000000001140  00001140
       0000000000000100  0000000000000010  AX       0     0     16
  [16] .text             PROGBITS         0000000000001240  00001240
       00000000000003d5  0000000000000000  AX       0     0     16
  [17] .fini             PROGBITS         0000000000001618  00001618
       000000000000000d  0000000000000000  AX       0     0     4
  [18] .rodata           PROGBITS         0000000000002000  00002000
       0000000000000030  0000000000000000   A       0     0     4
  [19] .eh_frame_hdr     PROGBITS         0000000000002030  00002030
       000000000000004c  0000000000000000   A       0     0     4
  [20] .eh_frame         PROGBITS         0000000000002080  00002080
       0000000000000128  0000000000000000   A       0     0     8
  [21] .init_array       INIT_ARRAY       0000000000003d20  00002d20
       0000000000000008  0000000000000008  WA       0     0     8
  [22] .fini_array       FINI_ARRAY       0000000000003d28  00002d28
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .dynamic          DYNAMIC          0000000000003d30  00002d30
       0000000000000210  0000000000000010  WA       7     0     8
  [24] .got              PROGBITS         0000000000003f40  00002f40
       00000000000000c0  0000000000000008  WA       0     0     8
  [25] .data             PROGBITS         0000000000004000  00003000
       0000000000000010  0000000000000000  WA       0     0     8
  [26] .bss              NOBITS           0000000000004010  00003010
       0000000000000010  0000000000000000  WA       0     0     16
  [27] .comment          PROGBITS         0000000000000000  00003010
       000000000000002b  0000000000000001  MS       0     0     1
  [28] .symtab           SYMTAB           0000000000000000  00003040
       00000000000007b0  0000000000000018          29    46     8
  [29] .strtab           STRTAB           0000000000000000  000037f0
       0000000000000316  0000000000000000           0     0     1
  [30] .shstrtab         STRTAB           0000000000000000  00003b06
       000000000000011a  0000000000000000           0     0     1

3. 启动进程

手工启动

  • 由用户输入命令直接启动进程 ls ./a.out
  • 前台运行和后台运行 ./run &

调度启动

  • 系统根据用户事先的设定自行启动进程
  • at
    • 在指定时刻执行相关进程
  • crontab
    • 周期性执行相关

4. 进程相关的几个命令

ps
	列出系统中当前运行的那些进程。
top
	实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器。
kill
	向Linux系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。
nice/renice
	优先级操作
bg
	将一个在后台暂停的命令,变成继续执行。
fg
	将后台中的命令调至前台继续运行

四、进程系统调用

1. fork()函数

功能:创建一个新的进程
头文件:#include <unistd.h>

原型:pid_t fork(void);
返回值:
	成功 0 或者大于 0 的正整数
	失败 -1
备注:
	该函数执行成功之后,将会产生一个新的子进程,在新的子进程中其返回值为 0,在原来的父进程中其返回值为大于 0 的正整数,该正整数就是子进程的 PID

以下代码显示了 fork( )的作用:

#include 
#include 

int main(int argc, char const *argv[])
{
    printf("[%d]\n", __LINE__);

    pid_t pid = fork();
    if (pid == 0) //子进程
    {
        printf("(child) PID: %d, PPID: %d\n", getpid(), getppid());
    }
    if (pid > 0) // 父进程,返回PID为子进程的父进程ID
    {
        printf("(parent) PID: %d, PPID: %d\n", getpid(), getppid());
    }
    
    printf("[%d]\n", __LINE__);
    return 0;
}

//运行测试:
sun@ubuntu:~/work/test$ ./fork 
[6]
(parent) PID: 21822, PPID: 2577
[18]
(child) PID: 21823, PPID: 21822
[18]

    
//结论:
fork函数本质上是系统底层复制了一份父进程的代码,一份代码执行pid>0的部分, 另外一份执行时pid=0的功能

//思考:既然创建子进程的意义是在于让其去执行某个功能,因此不只是单独的复制一份进程的代码,于是在pid=0的部分可以去执行相应的 ELF 文件或者脚本,用以覆盖从父进程复制过来的代码,因此引入exec函数

2. exit()和_exit()函数

进程内存机制及系统调用机制_第7张图片

功能: 退出本进程
头文件:
	#include 
	#include 

原型:void _exit(int status);
	 void exit(int status);
参数:
	status 子进程的退出值
返回值:
	不返回
备注:
	1,如果子进程正常退出,则 status 一般为 02,如果子进程异常退出,则 statuc 一般为非 03exit( )退出时,会自动冲洗(flush)标准 IO 总残留的数据到内核,如果进程注册了“退出处理函数”还会自动执行这些函数。而_exit( )会直接退出。

下代码展示了 exit( )和_exit( )的用法和区别:

sevan@ubuntu:~/ch05/5.2$ cat exit.c -n
include <stdio.h>
include <stdlib.h>
include <unistd.h>

void routine1(void) // 退出处理函数

printf("routine1 is called.\n");


void routine2(void) // 退出处理函数
{
	printf("routine2 is called.\n");
}

int main(int argc, char **argv)
{
    atexit(routine1); // 注册退出处理函数
    atexit(routine2);

    fprintf(stdout, "abcdef"); // 将数据输送至标准 IO 缓冲区

#ifdef _EXIT
	_exit(0); // 直接退出
#else
	exit(0); // 冲洗缓冲区数据,并执行退出处理函数
#endif
}
sevan@ubuntu:~/ch05/5.2$ gcc exit.c -o exit
sevan@ubuntu:~/ch05/5.2$ ./exit
abcdefroutine2 is called.
routine1 is called.
sevan@ubuntu:~/ch05/5.2$ gcc exit.c -o exit -D_EXIT
sevan@ubuntu:~/ch05/5.2$ ./exit
sevan@ubuntu:~/ch05/5.2$

通过以上操作可见,如果编译时不加-D_EXIT,那么程序将会执行 exit(0),那么字符串 abcdef 和两个退出处理函数(所谓的“退出处理函数”指的是进程使用 exit( ) 退出时被自动执行的函数,需要使用 atexit( )来注册)都被相应地处理了。而如果编译时加了-D_EXIT 的话,那么程序将执行_exit(0),从执行结果看,缓冲区数据没有被冲洗,退出处理函数也没有被执行。

这两个函数的参数 status 是该进程的退出值,进程退出后状态切换为 EXIT_ZOMBIE,相应地,这个值将会被放置在该进程的“尸体”(PCB)里面,等待父进程的回收。在进程异常退出时,有时需要向父进程汇报异常情况,此时就用非零值来代表特定的异常情况,比如 1 代表权限不足、2 代表内存不够等等,具体情况只要父子进程商定好就可以了。

3. exec函数簇

加载 ELF 文件或者脚本的接口函数

功能: 在进程中加载新的程序文件或者脚本,覆盖原有代码,重新运行
头文件: #include <unistd.h>
    
原型:
    int execl(const char *path, const char *arg, ...);
    int execv(const char *path, char *const argv[ ]);
    int execle(const char *path, const char *arg, ..., char * const envp[ ]);
    int execlp(const char *file, const char *arg, ...);
    int execvp(const char *file, char *const argv[ ]);
    int execvpe(const char *file, char *const argv[ ],char *const envp[ ]);
参数:
    path 即将被加载执行的 ELF 文件或脚本的路径
    file 即将被加载执行的 ELF 文件或脚本的名字
    arg 以列表方式罗列的 ELF 文件或脚本的参数
    argv 以数组方式组织的 ELF 文件或脚本的参数
    envp 用户自定义的环境变量数组
返回值:
	成功 不返回
	失败 -1
备注:
    1,函数名带字母 l 意味着其参数以列表(list)的方式提供。
    2,函数名带字母 v 意味着其参数以矢量(vector)数组的方式提供。
    3,函数名带字母 p 意味着会利用环境变量 PATH 来找寻指定的执行文件。
    4,函数名带字母 e 意味着用户提供自定义的环境变量。

上述代码组成一个所谓的“exec 函数簇”,因为他们都长得差不多,功能都是一样的,彼此间有些许区别(详见上表中的备注)。使用这些函数还要注意以下事实:

  • 被加载的文件的参数列表必须以自身名字为开始,以 NULL 为结尾。比如要加载执行当前目录下的一个叫做 a.out 的文件,需要一个参数”abcd”,那么正确的调用应该是:

    execl(./a.out”, “a.out”, “abcd”, NULL);
    或者:
    const char *argv[3] = {“a.out”, “abcd”, NULL};
    execv(./a.out”, argv);
    
  • exec 函数簇成功执行后,原有的程序代码都将被指定的文件或脚本覆盖,因此这些函数一旦成功后面的代码是无法执行的,他们也是无法返回的。

下面展示子进程被创建出来之后执行的代码,以及如何加载这个指定的程序。被子进程加载的示例代码:

sevan@ubuntu:~/ch05/5.2$ cat child_elf.c -n
#include 
#include 

int main(void)
{
    printf("[%d]: yep, I am the child\n", (int)getpid());
    exit(0);
}

下面是使用 exec 函数簇中的 execl 来让子进程加载上述代码的示例:

/*
 * @Descripttion: exec.c
 * @Author: Jaylen
 * @version: 
 * @Date: 2023-02-21 13:56:23
 * @LastEditors: Jaylen
 * @LastEditTime: 2023-02-21 14:05:29
 */
#include 
#include 
#include 

int main(int argc, char **argv)
{
    pid_t x;
    x = fork();

    if(x > 0) // 父进程
    {
        printf("[%d]: I am the parent\n", (int)getpid());
        exit(0);
    }

    if(x == 0) // 子进程
    {
        printf("[%d]: I am the child\n", (int)getpid());
        execl("./child_elf", "child_elf", NULL); // 执行 child_elf 程序
        
       // char *arg[] = {"./child_elf", NULL};
        //execv("./child_elf", arg);
        
        //测试
        //execl("/bin/ls","ls", "-l", NULL);
        //execlp("ls", "ls", "-l", NULL);

        printf("NEVER be printed\n"); // 这是一条将被覆盖的代码
    }

    return 0;
}

运行测试

//下面是执行结果:
sevan@sevan-vm:test$ ./exec 
[17457]: I am the parent
[17458]: I am the child
sevan@ubuntu:test$ [17458]: yep, I am the child

以上执行结果看到,父进程比其子进程先执行完代码并退出,因此 Shell 命令提示行又被夹在中间了,那么怎么让子进程先运行并退出之后,父进程再继续呢?子进程的退出状态又怎么传递给父进程呢?答案是:可以使用 exit()/_exit()来退出并传递退出值,使用wait()/waitpid()来使父进程阻塞(sè)等待子进程

4. wait( )/waitpid( )函数

父进程如果需要获得子进程正常退出的退出值,可以使用 wait( )/waitpid( )函数,这两个函数还可以使得父进程阻塞等待子进程的退出,以及将子进程状态切换为
EXIT_DEAD 以便于系统释放子进程资源。

功能:等待子进程
头文件:#include <sys/wait.h>

原型:
	pid_t wait(int *stat_loc);//阻塞等待
	pid_t waitpid(pid_t pid, int *stat_loc, int options); //非阻塞等待
参数
	pid
		小于-1:等待组 ID 的绝对值为 pid 的进程组中的任一子进程
		-1:等待任一子进程
		0:等待调用者所在进程组中的任一子进程
		大于 0:等待进程组 ID 为 pid 的子进程
	stat_loc 
    	子进程退出状态
	option
        WCONTINUED:报告任一从暂停态出来且从未报告过的子进程的状态
        WNOHANG:非阻塞等待
        WUNTRACED:报告任一当前处于暂停态且从未报告过的子进程的状态
返回值:
	wait( ) 
        成功:退出的子进程 PID
		失败:-1
	waitpid( )
		成功:状态发生改变的子进程 PID(如果 WNOHANG 被设置,且由 pid 指定的进程存在但状态尚未发生改变,则返回 0)。
	失败:-1
备注:如果不需要获取子进程的退出状态,stat_loc 可以设置为 NULL

注意,所谓的退出状态不是退出值,退出状态包括了退出值,退出状态为一个32位的整型值,前24位包含其他信息,最后8位才是退出码值。
进程内存机制及系统调用机制_第8张图片
如果使用以上两个函数成功获取了子进程的退出状态,则可以使用以下宏来进一步解析:
进程内存机制及系统调用机制_第9张图片
①正常退出指的是调用 exit( )/_exit( ),或者在主函数中调用 return,或者在最后一个线程调用 pthread_exit( )
下面是使用 wait函数和waitpid 父进程等待回收子进程资源的示例:

wait函数

#include 
#include 
#include 
#include 
#include 

/* wait */
        
int main(int argc, char **argv)
{
	pid_t pid;
	int status;

	printf("parent[pid=%d] is born\n", getpid());
		 
	if (-1 == (pid = fork())) {
		perror("fork error");
		return -1;
	}
	
	if (pid == 0){
		printf("child[pid=%d] is born\n", getpid());
		sleep(10);
		printf("child is over\n");
		exit(7);
		return 123;
	}
	else{
		pid_t pid_w;
		
		pid_w = wait(&status);//等待子进程退出 0x7b00
		if (pid_w < 0) {
			perror("wait error");
			return 1;
		}
		printf("status=%x \n",status);
		if (WIFEXITED(status)) {
			//正常退出
			status = WEXITSTATUS(status);//提取返回信息
			printf("wait returns with pid = %d. return status is %d\n", pid_w, status);
		} else {
			//非正常退出
			printf("wait returns with pid = %d. the child is terminated abnormally\n", pid_w);
		}		
		printf("father is over\n");		
		return 0;
	}
}

运行测试:

sun@ubuntu:test$ ./wait 
parent[pid=33144] is born
child[pid=33145] is born
child is over
status=700 
wait returns with pid = 33145. return status is 7
father is over

waitpid 函数

#include 
#include 
#include 
#include 
#include 

/* wait method: */
 /* waitpid*/
int main(int argc, char **argv)
{
	pid_t pid;

	printf("parent[pid=%d] is born\n", getpid());
		 
	if (-1 == (pid = fork())) {
		perror("fork error");
		return -1;
	}
	
	if (pid == 0){//子进程
		printf("child[pid=%d] is born\n", getpid());
		sleep(5);
		printf("child is over\n");
	}
	else{ //parent  父进程
		pid_t pid_w;
		
		while((pid_w = waitpid(pid, NULL, WNOHANG)) == 0) { //WNOHANG:子进程运行中返回0
			printf("parent wait w/o HAND and returns with 0\n");
			sleep(1);
		}
		printf("waitpid returns with pid = %d.\n", pid_w);
		
		printf("father is over\n");
	}
	
	return 0;
}

运行测试

sun@ubuntu:test$ ./waitpid 
parent[pid=33214] is born
parent wait w/o HAND and returns with 0
child[pid=33215] is born
parent wait w/o HAND and returns with 0
parent wait w/o HAND and returns with 0
parent wait w/o HAND and returns with 0
parent wait w/o HAND and returns with 0
child is over
waitpid returns with pid = 33215.
father is over

五、进程运行状态

下图给出 Linux 进程从被创建(生)到被回收(死)的全部状态,以及这些状态发生转换时的条件:
进程内存机制及系统调用机制_第10张图片
① 从“蛋生”可以看到,一个进程的诞生,是从其父进程调用 **fork( )**开始的。

② 进程刚被创建出来的时候,处于 TASK_RUNNING 状态,从图中看到,处于该状态的进程可以是正在进程等待队列中排队,也可以占用 CPU 正在运行,我们习惯上称前者为“就绪态”,称后者为“执行态”。

③ 刚被创建的进程都处于“就绪”状态,等待系统调度,内核中的函数 sched( )被称为调度器,他会根据各种参数来选择一个等待的进程去占用 CPU。进程占用 CPU 之后就可以真正运行了,运行时间有个限定,比如 20 毫秒,这段时间被称为 time slice,即“时间片”的概念。时间片耗光的情况下如果进程还没有结束,那么会被系统重新放入等待队列中等待。另外,正处于“执行态”的进程即使时间片没有耗光,也可能被别的更高优先级的进程“抢占”CPU,被迫重新回到等到队列中等待。

④ 进程处于“执行态”时,可能会由于某些资源的不可得而被置为“睡眠态/挂起态”,比如进程要读取一个管道文件数据而管道为空,或者进程要获得一个锁资源而当前锁不可获取,或者干脆进程自己调用 sleep( )来强制自己挂起,这些情况下进程的状态都会变成 TASK_INTERRUPIBLE 或者 TASK_UNINTERRUPIBLE,他们的区别是一般后者跟某些硬件设置相关,在睡眠期间不能响应信号,因此 TASK_UNINTERRUPIBLE 的状态也被称为深度睡眠,相应地 TASK_INTERRUPIBLE 期间进程是可以响应信号的。当进程所等待的资源变得可获取时,又会被系统置为 TASK_RUNNING 状态重新就绪排队.

⑤ 当 进程 收 到 SIGSTOP 或 者 SIGTSTP 中 的 其中 一 个 信 号 时, 状 态 会 被 置为TASK_STOPPED,此时被称为“暂停态”,该状态下的进程不再参与调度,但系统资源不释放,直到收到 SIGCONT 信号后被重新置为就绪态。当进程被追踪时(典型情况是被调试器调戏时,收到任何信号状态都会被置为 TASK_TRACED,该状态跟暂停态是一样的,一直要等到 SIGCONT 才会重新参与系统进程调度。

⑥ 运行的进程跟人一样,迟早都会死掉。进程的死亡可以有多种方式,可以是寿终正寝的正常退出,也可以是被异常杀死。比如上图中,在 main 函数内 return 或者调用 exit( ),包括在最后线程调用 pthread_exit( )都是正常退出,而受到致命信号死掉的情况则是异常死亡,不管怎么死,最后内核都会调用一个叫 do_exit( )的函数来使得进程的状态变成所谓的僵尸态 EXIT_ZOMBIE,这里的“僵尸”指的是进程的 PCB(进程控制块)。

问题思考

  • 为什么一个进程的死掉之后还要把尸体留下呢?因为进程在退出的时候,将其退出信息都封存在他的尸体里面了,比如如果他正常退出,那退出值是多少呢?如果被信号杀死?那么是哪个信号呢?这些“死亡信息”都被一一封存在该进程的 PCB 当中,好让别人可以清楚地知道:我是怎么死的。那谁会关心他是怎么死的呢?

    答案: 他的父进程,他的父进程之所以要创建他,很大的原因是要让这个孩子去干某一件事情,现在这个孩子已死,那事情办得如何,孩子是否需要有个交代?但他又死掉了,所以之后将这些“死亡信息”封存在自己的尸体里面,等着父进程去查看,比如父子进程可以约定:如果事情办成了退出值为 0,如果权限不足退出值为 1,如果内存不够退出值为 2 等等。父进程可以随时查看一个已经死去的孩子的事情究竟办得如何。可以看到,在工业社会中,哪怕是进程间的协作,也充满了契约精神。

  • 父进程调用 wait()/waitpid()来查看孩子的“死亡信息”,顺便做一件非常重要的事情:将该孩子的状态置为 EXIT_DEAD,即死亡态,因为处于这个状态的进程的 PCB才能被系统回收。父进程可以尽职尽责地及时的调用 wait()/waitpid(),以此避免系统充满越来越多的“僵尸”嘛?

    答案是不能,因为父进程也许需要做别的事情没空去帮那些死去的孩子收尸父进程有别的事情要干,不能随时执行 wait( ) / waitpid( )来确保回收僵尸资源。在这样的情形下,我们可以考虑使用信号异步通知机制,让一个孩子在变成僵尸的时候,给其父进程发一个信号,父进程接收到这个信号之后,在对其进行处理,在此之前想干嘛就干嘛,异步操作,大家 happy。但是即便是这样也仍然存在问题:如果两个以上的孩子同时退出变僵尸,那么他们就会同时给其父进程发送相同的信号,而相同的信号将会被淹没。

  • 在子进程未变成僵尸状态时,父进程已经先他而去的情况如何处理?

    答案:如果一个进程的父进程退出,那么祖先进程 init该进程是系统第一个运行的进程,他的 PCB 是从内核的启动镜像文件中直接加载的,不需要别的进程 fork( )出来,因此他是无父无母的石头爆出来的,系统中的所有其他进程都是他的后代)将会收养(adopt)这些孤儿进程。

    换句话说:Linux 系统保证任何一个进程(除了init)都有父进程,也许是其真正的生父,也许是其祖先init

僵尸进程及处理机制

僵尸进程:子进程退出了,但是父进程没有用wait或waitpid去获取子进程的状态信息,那么子进程的进程描述符(包括进程号 PID,退出状态 the termination status of the process,运行时间 the amount of CPU time taken by the process 等)仍然保存在系统中,这种进程称为僵尸进程。

ps 命令查看进程的状态:ps aux|grep zomprodemo

僵尸进程的处理机制:

  • kill杀死元凶父进程(一般不用)
    严格的说,僵尸进程并不是问题的根源,罪魁祸首是产生大量僵死进程的父进程。因此,我们可以直接除掉元凶,通过kill发送SIGTERM或者SIGKILL信号。元凶死后,僵尸进程进程变成孤儿进程,由init充当父进程,并回收资源。或者运行:kill -9 父进程的pid值、(僵尸进程无法用kill直接杀死)

  • 父进程用wait或waitpid去回收资源(方案不好)
    父进程通过wait或waitpid等函数去等待子进程结束,但是不好,会导致父进程一直等待被挂起,相当于一个进程在干活,没有起到多进程的作用。

  • 通过信号机制,在处理函数中调用wait,回收资源(推荐)
    通过信号机制,子进程退出时向父进程发送SIGCHLD信号,父进程调用signal(SIGCHLD,sig_child) 去处理SIGCHLD信号,在信号处理函数sig_child()中调用wait进行处理僵尸进程。什么时候得到子进程信号,什么时候进行信号处理,父进程可以继续干其他活,不用去阻塞等待。

六、代码实例

以下示例代码,综合展示了如果正确使用 fork()/exec()函数簇,exit()/_exit()和 wait()/waitpid(),程序功能是:父进程产生一个子进程让他去程序 child_elf,并且等待他的退出(可以用wait() 阻塞等待,也可以用 waitpid()非阻塞等待),子进程退出(可以正常退出,也可以异常退出)后,父进程获取子进程的退出状态后打印出来。详细代码如下:

/*
 * @Descripttion: child_elf.c
 * @Author: Jaylen
 * @version: 
 * @Date: 2023-02-21 11:53:19
 * @LastEditors: Jaylen
 * @LastEditTime: 2023-02-21 13:44:57
 */
#include 
#include 
#include 
#include 


int main(void)
{
	printf("[%d]: yep, I am the child\n", (int)getpid());

#ifdef ABORT
	abort(); // 自己给自己发送一个致命信号 SIGABRT,自杀
#else
	exit(7); // 正常退出,且退出值为 7
#endif
}

/*
 * @Descripttion: wait.c
 * @Author: Jaylen
 * @version:
 * @Date: 2023-02-21 11:54:18
 * @LastEditors: Jaylen
 * @LastEditTime: 2023-02-21 13:41:57
 */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 
#include 


int main(int argc, char **argv)
{
    pid_t pid = fork();

    if (pid == 0) // 子进程,执行指定程序 child_elf
    {
        execl("./child_elf", "child_elf", NULL);
    }

    if (pid > 0) // 父进程,使用 wait( )阻塞等待子进程的退出
    {
        int status;
        wait(&status); //阻塞等待

        /*非阻塞等待*/
       // waitpid(pid, &status, WNOHANG);//如果子进程退出进入死亡态啦,那么WNOHANG选项会立刻将子进程回收,如子进程没有死亡还在执行相关功能,那么父进程会立即退出,不会等待

        if (WIFEXITED(status)) // 判断子进程是否正常退出
        {
            printf("child exit normally, "
                   "exit value: %hhu\n",
                   WEXITSTATUS(status));
        }

        if (WIFSIGNALED(status)) // 判断子进程是否被信号杀死
        {
            printf("child killed by signal: %u\n",
                   WTERMSIG(status));
        }
    }

    return 0;
}

运行测试:

sevan@sevan-vm:test$ gcc child_elf.c -o child_elf
sevan@sevan-vm:test$ gcc wait.c -o wait
sevan@sevan-vm:test$ ./wait 
[15705]: yep, I am the child
child exit normally, exit value: 7

sevan@sevan-vm:test$ gcc child_elf.c -o child_elf -DABORT
sevan@sevan-vm:test$ ./wait 
[16641]: yep, I am the child
child killed by signal: 6

可以看到,子进程不同的退出情形,父进程的确可以通过 wait( )/waitpid( )和一些相应的宏来获取,这是协调父子进程工作的一个重要的途径。

以上部分内容来自《LINUX环境编程图文指南》

你可能感兴趣的:(Linux系统应用,linux,进程)