进程内存机制及API及详解

news/2024/4/24 6:38:21/文章来源:https://blog.csdn.net/weixin_40209493/article/details/129141694

一、进程概念

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

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

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

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

二、进程的内存布局

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

从上图可以看到,一个用户进程可以访问的内存区域介于 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. 总结说明

以下以程序中在内存中的具体分布来具体说明内存区域的情况
在这里插入图片描述

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

三、进程操作

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

在这里插入图片描述

2. 编译命令:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PxIBfCl-1676959876812)(pic/02_compile_command.png)]
在这里插入图片描述
readelf 命令查看elf 文件状态

sevan@ubuntu:TCP_blocking_model$ readelf client -S
There are 31 section headers, starting at offset 0x3c20:节头:[号] 名称              类型             地址              偏移量大小              全体大小          旗标   链接   信息   对齐[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .interp           PROGBITS         0000000000000318  00000318000000000000001c  0000000000000000   A       0     0     1[ 2] .note.gnu.propert NOTE             0000000000000338  000003380000000000000020  0000000000000000   A       0     0     8[ 3] .note.gnu.build-i NOTE             0000000000000358  000003580000000000000024  0000000000000000   A       0     0     4[ 4] .note.ABI-tag     NOTE             000000000000037c  0000037c0000000000000020  0000000000000000   A       0     0     4[ 5] .gnu.hash         GNU_HASH         00000000000003a0  000003a00000000000000028  0000000000000000   A       6     0     8[ 6] .dynsym           DYNSYM           00000000000003c8  000003c80000000000000228  0000000000000018   A       7     1     8[ 7] .dynstr           STRTAB           00000000000005f0  000005f00000000000000124  0000000000000000   A       0     0     1[ 8] .gnu.version      VERSYM           0000000000000714  00000714000000000000002e  0000000000000002   A       6     0     2[ 9] .gnu.version_r    VERNEED          0000000000000748  000007480000000000000050  0000000000000000   A       7     2     8[10] .rela.dyn         RELA             0000000000000798  0000079800000000000000d8  0000000000000018   A       6     0     8[11] .rela.plt         RELA             0000000000000870  000008700000000000000180  0000000000000018  AI       6    24     8[12] .init             PROGBITS         0000000000001000  00001000000000000000001b  0000000000000000  AX       0     0     4[13] .plt              PROGBITS         0000000000001020  000010200000000000000110  0000000000000010  AX       0     0     16[14] .plt.got          PROGBITS         0000000000001130  000011300000000000000010  0000000000000010  AX       0     0     16[15] .plt.sec          PROGBITS         0000000000001140  000011400000000000000100  0000000000000010  AX       0     0     16[16] .text             PROGBITS         0000000000001240  0000124000000000000003d5  0000000000000000  AX       0     0     16[17] .fini             PROGBITS         0000000000001618  00001618000000000000000d  0000000000000000  AX       0     0     4[18] .rodata           PROGBITS         0000000000002000  000020000000000000000030  0000000000000000   A       0     0     4[19] .eh_frame_hdr     PROGBITS         0000000000002030  00002030000000000000004c  0000000000000000   A       0     0     4[20] .eh_frame         PROGBITS         0000000000002080  000020800000000000000128  0000000000000000   A       0     0     8[21] .init_array       INIT_ARRAY       0000000000003d20  00002d200000000000000008  0000000000000008  WA       0     0     8[22] .fini_array       FINI_ARRAY       0000000000003d28  00002d280000000000000008  0000000000000008  WA       0     0     8[23] .dynamic          DYNAMIC          0000000000003d30  00002d300000000000000210  0000000000000010  WA       7     0     8[24] .got              PROGBITS         0000000000003f40  00002f4000000000000000c0  0000000000000008  WA       0     0     8[25] .data             PROGBITS         0000000000004000  000030000000000000000010  0000000000000000  WA       0     0     8[26] .bss              NOBITS           0000000000004010  000030100000000000000010  0000000000000000  WA       0     0     16[27] .comment          PROGBITS         0000000000000000  00003010000000000000002b  0000000000000001  MS       0     0     1[28] .symtab           SYMTAB           0000000000000000  0000304000000000000007b0  0000000000000018          29    46     8[29] .strtab           STRTAB           0000000000000000  000037f00000000000000316  0000000000000000           0     0     1[30] .shstrtab         STRTAB           0000000000000000  00003b06000000000000011a  0000000000000000           0     0     1

3. 启动进程

手工启动

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

调度启动

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

4. 进程相关的几个命令

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

四、进程API及函数详解

1. fork()函数

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

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

#include <unistd.h>
#include <stdio.h>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()函数

在这里插入图片描述

功能: 退出本进程
头文件:#include <unistd.h>#include <stdlib.h>原型: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); // 直接退出
#elseexit(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 <stdio.h>
#include <stdlib.h>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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>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 子进程退出状态optionWCONTINUED:报告任一从暂停态出来且从未报告过的子进程的状态WNOHANG:非阻塞等待WUNTRACED:报告任一当前处于暂停态且从未报告过的子进程的状态
返回值:wait( ) 成功:退出的子进程 PID失败:-1waitpid( )成功:状态发生改变的子进程 PID(如果 WNOHANG 被设置,且由 pid 指定的进程存在但状态尚未发生改变,则返回 0)。失败:-1
备注:如果不需要获取子进程的退出状态,stat_loc 可以设置为 NULL

注意,所谓的退出状态不是退出值,退出状态包括了退出值,退出状态为一个32位的整型值,前24位包含其他信息,最后8位才是退出码值。
在这里插入图片描述
如果使用以上两个函数成功获取了子进程的退出状态,则可以使用以下宏来进一步解析:
在这里插入图片描述
①正常退出指的是调用 exit( )/_exit( ),或者在主函数中调用 return,或者在最后一个线程调用 pthread_exit( )

五、进程运行状态

下图给出 Linux 进程从被创建(生)到被回收(死)的全部状态,以及这些状态发生转换时的条件:
在这里插入图片描述
① 从“蛋生”可以看到,一个进程的诞生,是从其父进程调用 **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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>int main(void)
{printf("[%d]: yep, I am the child\n", (int)getpid());#ifdef ABORTabort(); // 自己给自己发送一个致命信号 SIGABRT,自杀
#elseexit(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 <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>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: 7sevan@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环境编程图文指南》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_72119.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【C++】thread|mutex|atomic|condition_variable

本篇博客&#xff0c;让我们来认识一下C中的线程操作 所用编译器&#xff1a;vs2019 阅读本文前&#xff0c;建议先了解线程的概念 &#x1f449; 线程概念 1.基本介绍 在不同的操作系统&#xff0c;windows、linux、mac上&#xff0c;都会对多线程操作提供自己的系统调用接口…

linux集群技术(二)--keepalived(高可用集群)(一)

高可用集群简介keepalived简介 1.高可用集群简介 1.1什么是高可用集群 高可用集群&#xff08;High Availability Cluster&#xff0c;简称HA Cluster&#xff09;&#xff0c;是指以减少服务中断时间为目的的服务器集群技术。它通过保护用户的业务程序对外不间断提供的服务&am…

智慧公厕系统为管理方提供更丰富的管理手段

很多时候&#xff0c;当人们外出游玩、在写字楼办公、商场购物、乘坐地铁火车出行时&#xff0c;都会看到公厕前面会有排队的现象&#xff0c;特别是对于人口流动大&#xff0c;公厕设施少的公共区域&#xff0c;队伍更是极其的长。智慧公厕可以解决传统公厕的脏乱差、异味和管…

python实现快速排序算法

文章目录1. 快速排序2. 步骤3. 详细代码4. 性能5. 相关链接1. 快速排序 快速排序&#xff08;英语&#xff1a;Quicksort&#xff09;&#xff0c;又称划分交换排序&#xff08;partition-exchange sort&#xff09;&#xff0c;通过一趟排序将要排序的数据分割成独立的两部分…

小公司“混”的3年,我认真做了5件事,真的受益终生

小公司“混”的3年&#xff0c;我认真做了5件事&#xff0c;真的受益终生 目录&#xff1a;导读 功能测试很重要但不值钱 自动化测试在小公司没市场&#xff0c;但是你得会 给自己的一些忠告 第一件事&#xff1a;分清阶段&#xff0c;制定计划 第二件事&#xff1a;梳理…

HTTP安全与HTTPS协议

目录 Http协议的安全问题 常见的加密方式 防止窃听 单向散列函数 单向散列值的特点 加密与解密 对称加密与非对称加密 对称加密的密钥配送问题 密钥配送问题的解决 非对称加密 前言&#xff1a; 公钥与私钥 非对称加密过程 混合密码系统 前言&#xff1a; 混合…

央行罚单!金融机构被罚原因揭秘

近日&#xff0c;人民银行公布了2023年首批行政处罚罚单&#xff0c;引发业内广泛关注。 顶象防御云业务安全情报中心统计了人民银行官网&#xff0c;2020年1月至2023年2月10日期间&#xff0c;公布的101份行政处罚。 统计显示&#xff0c;16家金融机构被罚27066.9万元&#…

易点天下基于 StarRocks 全面构建实时离线一体的湖仓方案

作者&#xff1a;易点天下数据平台团队易点天下是一家技术驱动发展的企业国际化智能营销服务公司&#xff0c;致力于为客户提供全球营销推广服务&#xff0c;通过效果营销、品牌塑造、垂直行业解决方案等一体化服务&#xff0c;帮助企业在全球范围内高效地获取用户、提升品牌知…

【Linux】vim拒绝服务安全漏洞修复

根据国家信息安全漏洞共享平台于2023年2月19日发布的安全漏洞通知&#xff0c;Linux系统自带的vim编辑器存在两个高危安全漏洞&#xff08;CNVD-2023-09166、CNVD-2023-09647&#xff09;&#xff0c;攻击者可以利用该漏洞发起拒绝服务攻击&#xff0c;并可能运行&#xff08;恶…

CAS 和 synchronized 优化过程

CAS: CAS相对于计算器&#xff08;count&#xff09;来说&#xff0c;count在多线程的环境下是线程不安全的&#xff0c;那么就必须得加锁&#xff0c;而加了锁性能就会大打折扣&#xff0c;所以就有了CAS而CAS的操作是原子的&#xff0c;从而会保证线程的安全。本质操作是将线…

列表推导式_Python教程

内容摘要 Python中存在一种特殊的表达式&#xff0c;名为推导式&#xff0c;它的作用是将一种数据结构作为输入&#xff0c;再经过过滤计算等处理&#xff0c;最后输出另一种数据结构。根据数据结构的不同会被分为列表推导式、 文章正文 Python中存在一种特殊的表达式&#x…

2022年网络安全政策态势分析与2023年立法趋势

近日&#xff0c;公安部第三研究所网络安全法律研究中心与 360 集团法务中心联合共同发布了《全球网络安全政策法律发展年度报告&#xff08;2022&#xff09;》。《报告》概览2022年全球网络安全形势与政策法律态势&#xff0c;并对2023年及后续短期内网络安全政策、立法趋势进…

TCP状态详解

TCP Tcp wrappers : Transmission Control Protocol (TCP) Wrappers 为由 inetd 生成的服务提供了增强的安全性。TCP Wrappers 是一种对使用 /etc/inetd.sec 的替换方法。TCP Wrappers 提供防止主机名和主机地址欺骗的保护。欺骗是一种伪装成有效用户或主机以获得对系统进行未…

linux集群技术(二)--keepalived(高可用集群)(二)

案例1--keepalived案例2--keepalived Lvs集群1.案例1--keepalived 1.1 环境 初识keepalived&#xff0c;实现web服务器的高可用集群。 Server1: 192.168.26.144 Server2: 192.168.26.169 VIP: 192.168.26.190 1.2 server1 创建etc下的…

网上插画教学哪家质量好,汇总5大插画培训班

网上插画教学哪家质量好&#xff1f;给大家梳理了国内5家专业的插画师培训班&#xff0c;最新五大插画班排行榜&#xff0c;各有优势和特色&#xff01; 一&#xff1a;国内知名插画培训机构排名 1、轻微课&#xff08;五颗星&#xff09; 主打课程有日系插画、游戏原画、古风插…

2023年测试人跳槽新功略,涨薪10K+

软件测试是如何实现涨薪的呢&#xff1f;很多人眼中的软件测试岗位可能是简单的&#xff0c;技术含量不是那么高&#xff0c;就是看看需求、看业务、设计文档、然后点一点功能是否实现&#xff0c;再稍微深入一点就是测试下安装部署时会不会出现兼容性问题&#xff0c;以及易用…

技术学习-消息队列

什么是消息队列 可以简单理解为存放消息的队列&#xff0c;数据结构模型和队列一样&#xff0c;都是先进先出。主要用不同线程(Thread)/进程(Process) 为什么需要消息队列 (1)不同进程之间传递消息是&#xff0c;因为进程的耦合度高&#xff0c;改动一个进程&#xff0c;引发…

npm 上传自己的包

mkdir demo 创建一个新的文件夹 npm init 初始化项目 生成一个package.json文件 name version description等等touch index.js 创建一个node 可执行脚本新的js 文件 #!/usr/bin/env node // 必须在文件头加如上内容指定运行环境为node console.log(hello cli)在package.json 中…

【教程】GitStats代码统计工具(附GitLab API相关)

使用GitStats进行代码统计 官方文档&#xff1a;GitStats - git history statistics generator GitStats是基于Git的数据统计生成器&#xff0c;输出格式为HTML&#xff0c;可直接在浏览器打开查看&#xff0c;展现为图表形式的可视化数据&#xff0c;内容包括&#xff1a; 常…

图像识别技术解析:手写数字识别(一)

本文通过构建一个手写数字识别的程序来解析来自机器学习与深度学习的不同算法的特点&#xff0c;以及如何对识别效果进行改进。 一、如何构建一个手写数字识别程序 首先可以考虑构建一个简单的页面用于用户输入&#xff0c;也就是前端&#xff1b;接下来需要准备一个后端用于…