1、孤儿进程

  • 孤儿进程是一个比父进程存活时间更长的进程
  • 孤立进程被init所采用
  • Init等待被收养的子进程终止
  • 采用孤儿进程后,getppid()返回init的PID;通常下init的PID为1
  • 在使用upstart作为init system的系统上,或者在某些配置中使用systemd的系统上,情况是不同的

父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”此时的子进程叫做孤儿进程。====爹没了

Linux避免系统存在过多的孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。====init养父

实例:

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

int main(){
    pid_t fpid;
    fpid = fork();

    if(fpid == -1){
        printf("fork error.\n");
        exit(1);
    }
    else if(fpid == 0)  //child process
    {
        printf("I'm child process, child pid = %d, parent pid = %d\n",getpid(),getppid());
        sleep(5);   //睡眠5s,保证父进程退出
        printf("I'm sleep. child pid = %d, parent pid = %d\n",getpid(),getppid());
        printf("child process is done.\n");
    }
    else{
        printf("I'm parent process.\n");
        sleep(1);  //睡眠1s
        printf("parent process is done.\n");
    }
    return 0;
}

【注意】

getpid函数可以获得当前进程的pid,getppid函数可以获得当前进程的父进程号。

结果:

【Linux】僵尸、孤儿进程,地址空间_僵尸进程

首先打印子进程和父进程的ID,后来父进程提前终结,子进程成为孤儿进程,打印子进程和init父进程ID。


2、僵尸进程

2.1、理解

创建子进程后,子进程退出状态不被收集,变成僵尸进程。爹不要它了

除非爹死后变孤儿进程,然后被init养父接收。如果父进程是死循环,那么该僵尸进程就变成游魂野鬼消耗空间。

  • 假设子进程在父进程等待它之前终止
  • 父进程必须仍能收集状态
  • 子进程变成僵尸进程
  • 大多数流程资源都是循环利用的
  • 保留一个进程槽位:PID、状态和资源使用统计
  • 当父节点执行“wait”操作时,僵尸将被移除

即:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。


【注意】

僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态可以提供父进程想要的信息。一旦父进程得到想要的信息,僵尸进程就会结束。


2.2、僵尸进程是怎样产生的

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用 exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。


在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集;


除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。


2.3、怎么查看僵尸进程

利用命令:ps,可以看到有标记为 的进程就是僵尸进程。


2.4、怎么清除僵尸进程

  • 方法一:改写父进程,在子进程死后要为它收尸。

具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。


  • 方法二

把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。

注:僵尸进程将会导致资源浪费,而孤儿则不会。


实例1以下是一个僵尸进程的示例程序,在此程序中,子进程先退出,父进程不调用wait()或waitpid()清理子进程信息。

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

int main(){
    pid_t fpid;
    fpid = fork();

    if(fpid == -1){
        printf("fork error.\n");
        exit(1);
    }
    else if(fpid == 0)  //child process
    {
        printf("I'm child process. pid = %d\n",getpid());
        exit(0);            //#退出进程,变成僵尸进程
    }
    else{
        printf("I'm parent process. I will sleep two seconds\n");
        sleep(2);
        system("ps -opid,ppid,state,tty,command");
        printf("father process is exiting.\n");
    }
    return 0;
}

【Linux】僵尸、孤儿进程,地址空间_僵尸进程_02

说明:子进程变成了僵尸进程


实例2父进程循环创建子进程,子进程退出,造成多个僵尸进程。

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

void cream_many_zombie(void){
    pid_t pid;
    while(1){
        pid = fork();
        if(pid < 0){
            perror("fork error.\n");
            exit(1);
        }
        else if(pid == 0){
            printf("I am a child. pid = %d\nI am is existing.\n",getpid());
            exit(0);   //子进程退出,变成僵尸进程
        }
        else{
            printf("---------------------------\n");
            printf("I am parent process.\n");
            system("ps -opid,ppid,state,tty,command");
            printf("---------------------------\n");
            sleep(4);
            continue;
        }
    }
    return;
}

int main(){
    cream_many_zombie();
    return 0;
}

【Linux】僵尸、孤儿进程,地址空间_僵尸进程_03


*3、僵尸进程解决方法

*3.1、通过信号控制

子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:

【Linux】僵尸、孤儿进程,地址空间_父进程_04

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

static void signale_handler(int signo){
    pid_t pid;
    int stat;
    //处理僵尸进程
    while((pid = waitpid(-1,&stat,WNOHANG)) > 0){
        printf("child %d terminated.\n",pid);
    }
}

void sigchld_zombie(void){
    pid_t pid;
    // signal(SIGCHLD, signale_handler);
    struct sigaction sa;
    sa.sa_handler = signale_handler;
    sa.sa_flags = SA_NOCLDSTOP ;
    sigemptyset(&sa.sa_mask);
    if(sigaction(SIGCHLD,&sa,NULL) == -1){
        perror("sigaction");
    }
    pid = fork();
    if(pid == -1){
        printf("fork error.\n");
        exit(1);
    }
    else if(pid == 0)  //child process
    {
        printf("I am child process,pid = %d.\nI am exiting.\n",getpid());
        exit(0);
    }
    else{
        sleep(2);
        //输出进程信息
        system("ps -opid,ppid,state,tty,command");
        printf("father process isexiting.\n");
    }
    return;
}

int main(){
    sigchld_zombie();
    return 0;
}

【Linux】僵尸、孤儿进程,地址空间_子进程_05

3.2、两次fork()

实现思路:将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

  1. 在子进程中再创建一个子进程,相当于第一个子进程就是第二个子进程的父进程.
  2. 当sleep(3)后,要确保第一个进程(第二个进程的父进程)退出,那么第二个进程就变成了孤儿进程
  3. 然后init进程会过来接手处理。
  4. 所有到最后,第一个进程的Pid就变成了init的pid。
#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>

void doble_fork_zombie(void){
    pid_t pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
        //子进程再创建子进程
        printf("\nI am the first child. pid:%d\tppid:%d\n",getpid(),getppid());
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        //第一个子进程退出
        else if (pid > 0)
        {
            printf("first process is exited.\n");
            exit(0);
        }
        //第二个子进程
        //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程
        sleep(3);
        printf("I am the second child. pid: %d\tppid:%d\n",getpid(),getppid());
        system("ps -opid,ppid,state,tty,command");
        exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
        perror("waitepid error:");
        exit(1);
    }
    exit(0);
}

int main(){
    doble_fork_zombie();
    return 0;
}

【Linux】僵尸、孤儿进程,地址空间_父进程_06



4、地址空间

4.1、内存究竟是什么?分为哪些?

编程的时候我们经常和地址打交道,我们认为内存的布局是这样的,分成了许多不同的区域,不同区域用于存放不同数据,比如说局部变量存放在栈里,全局变量放在堆里,等等,同时我们认为地址是从下向上递增的.我们在调试的时候看到的地址也是这个地址,俗称进程地址空间。但其实系统真正的存储空间方式并不是这个而是物理地址空间。进程地址空间其实是一种数据结构,每一个进程都有自己的进程地址空间

计算机的内存大小是固定的通常是4gb,8gb,16gb。而每个进程地址空间的大小都是内存的大小,这样一来对于每个进程而言,进程本身认为它可用的空间都是整个个内存大小,不同进程之间不知道其他进程的存在.

【Linux】僵尸、孤儿进程,地址空间_父进程_07

验证C/C++中各区域的相对位置

#include <stdio.h>
 #include <stdlib.h>
 #include <unistd.h>
 
 int un_global_val;
 int Init_global_val = 100;
 int main(int argc, char *argv[], char * env[])
 {
 	printf("code addr: %p\n", main); //代码区
 	
     const char *str = "hello Linux";//字符常量区
     printf("read only char add: %p\n", str);
     printf("Init global value add: %p\n", &Init_global_val);//全局初始区
     printf("uninit global value add: %p\n", &un_global_val);//全局未初始区
 
     char* heap1 = (char*)malloc(100);
     char* heap2 = (char*)malloc(100);
     char* heap3 = (char*)malloc(100);
     char* heap4 = (char*)malloc(100);
     
     //堆及地址增长方向
     printf("heap1 add: %p\n", heap1);
     printf("heap2 add: %p\n", heap2);
     printf("heap3 add: %p\n", heap3);
     printf("heap4 add: %p\n", heap4);
     //堆及地址增长方向
     printf("stack1 add: %p\n", &heap1);                                                                                                                
     printf("stack2 add: %p\n", &heap2);
     printf("stack3 add: %p\n", &heap3);
     printf("stack4 add: %p\n", &heap4);
     
     int i = 0;//命令行参数
     for(; argv[i]; i++)
     {
         printf("argv[%d]: %p\n",i, argv[i]);
     }
     
     i = 0;//环境变量
     for(; i < 2; i++)
     {
         printf("env[%d]: %p\n",i, env[i]);
     }
     return 0;
 }

结果:

【Linux】僵尸、孤儿进程,地址空间_子进程_08


4.2、内存是真实物理空间?

用fork创建一个子进程,并打印父进程和子进程对于的pid、ppid、全局变量值、全局变量地址。当子进程执行2次后,子进程修改全局变量。

#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
    
int global_val = 100;    
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
       //child    
       int cnt = 3;                                                                                                                                      
       while(1)    
       {    
           printf("child Pid:%d Ppid:%d g_val:%d &g_val:%p\n", getpid(), getppid(), global_val, &global_val);    
           if(--cnt == 0)    
           {    
               global_val = 200;    
               printf("child change g_val 100 -> 200\n");    
           }    
           sleep(1);    
       }    
    }    
    else if(id > 0)    
    {    
        //pather          
       while(1)    
       {    
           printf("father Pid:%d Ppid:%d g_val:%d &g_val:%p\n", getpid(), getppid(), global_val, &global_val);    
           sleep(1);    
       }    
    }    
    return 0; 
}

结果:

【Linux】僵尸、孤儿进程,地址空间_地址空间_09

  • 我们发现fork()创建的子进程对全局变量进行修改后,毋庸置疑父进程和子进程的值不同。但奇怪的是,父/子进程中,全局变量的地址竟然是一样的,未发生改变!
  • 一块地址空间的值是唯一的。但上述现象中出现同一块变量却存在不同的值,说明父子进程中显示的地址不是真实的物理地址。在LInux中,我们将这种地址称之为虚拟地址。
  • 在C/C++中,我们看到的地址都是虚拟地址(进程地址空间)。真实地址都是由操作系统进行控制分配的,用户一概不知道!!


4.3、进程地址空间(虚拟地址)

  1. 为何同一个变量地址相同,保存的数据却不同?
  • 原因在于在Linux中,操作系统会为每一个进程维护一个PCB、进程地址空间(也被称为虚拟地址空间)和页表。其中页表通过映射,将虚拟地址和真实物理地址进行藕接。操作系统可以通过页表,找到虚拟地址所对应的真实物理地址,进而找到相应的数据!!
  • 当fork()创建子进程时,操作系统以父进程为模板将大部分属性拷贝给子进程,而页表就是其中之一。在数据未发生任何改变之前,父进程和子进程中页表的映射关系是相同的,指向同一块物理地址。但当父进程或子进程试图对数据进行修改时,为了保证父进程和子进程之间的独立性,操作系统会为该进程申请创建新的空间,然后将页表中的映射关系进行修改,指向新申请的物理空间。换而言之,数据不修改时指向同一块物理地址;数据修改后,各自私有一份!
  • 但在此过程中,操作系统仅仅是将页表中的映射关系进行修改。我们所看到的地址(虚拟地址)和变量并未发生改变。这也就意味着当父进程和子进程运行时,父进程和子进程的PCB、进程地址空间、和页表都是相互独立的,各自私有一份。尽管虚拟地址相同,但页表中的映射关系已经发生改变,此时我们获取虚拟地址对应的数据时,操作系统通过页表映射到不同的物理地址,从而获取到不同的值!

实例:

  1. 创建子进程时,未发生数据修改。此时操作系统会以父进程为模板将大部分数据拷贝给子进程,并且共用代码。其中父进程页表中的数据直接拷贝给子进程页表。此时子进程中的虚拟地址通过页表的映射关系,和父进程指向同一块物理空间!

【Linux】僵尸、孤儿进程,地址空间_地址空间_10

  1. 当子进程中的数据发生修改时,为了保证父进程和子进程的独立性,操作系统会为子进程的真实物理空间重新开辟一块空间,用于存储修改后的值。之后将子进程的页表中的映射关系进行调整,指向新空间。

【Linux】僵尸、孤儿进程,地址空间_子进程_11


5、为什么需要地址空间和页表的存在?

假如没有进程地址空间,我们的数据都直接储存在物理地址空间,也就是真正的内存上,这种做法其实是非常危险的。因为内存是可以随时被读写的,这意味每个进程不再是独立的,不同进程之间有可能读取到对方的数据,这是很不安全的,例如因代码的问题造成野指针的问题就会导致意外发生。

[all in all]

地址空间能有效的保护物理内存,同时可以延迟分配空间,提高效率,因为一开始创建进程地址空间的时候是没有给进程分配一丁点空间的。同时因为页表的存在,实现了进程管理模块和内存管理模块的解耦合,从而也实现了对进程任意位置的加载。因为页表本身的储存的物理地址是会随着进程的运行而更新的.同时页表不仅仅可以映射物理内存,也可以映射磁盘