进程概念

发布时间 2023-09-04 19:21:16作者: C_asdfgh

1. 基本概念

  • 程序:编译后产生的,格式为ELF的,存储于硬盘的文件
  • 进程:程序中的代码和数据,被加载到内存中运行的过程
  • 程序是静态的概念,进程是动态的概念
img ELF格式程序与进程

在Linux中,程序文件的格式都是ELF,这些文件在被执行的瞬间,就被载入内存,所谓的载入内存,如上图所示,就是将数据段、代码段这些运行时必要的资源拷贝到内存,另外系统会再分配相应的栈、堆等内存空间给这个进程,使之成为一个动态的实体。

2. 进程的组织方式

在Linux系统中,除了系统的初始进程之外,其余所有进程都是通过从一个父进程(parent)复刻(fork)而来的,有点像人类社会,每个个体都是由亲代父母繁衍而来。

因此,在整个Linux系统中,所有的进程都起源于相同的初始进程,它们之间形成一棵倒置的进程树,就像家族族谱,可以使用命令pstree查看这些进程的关系:

gec@ubuntu:~$ 
gec@ubuntu:~$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]
        ├─VGAuthService
        ├─accounts-daemon───2*[{accounts-daemon}]
        ├─acpid
        ├─avahi-daemon───avahi-daemon
        ├─bluetoothd
        ├─boltd───2*[{boltd}]
        ├─colord───2*[{colord}]
        ├─cron
        ├─cups-browsed───2*[{cups-browsed}]
        ├─cupsd
        ├─2*[dbus-daemon]
        ├─fcitx
        ├─fcitx-dbus-watc
        ├─firefox─┬─Privileged Cont───18*[{Privileged Cont}]
        │         ├─Web Content───15*[{Web Content}]
        │         ├─Web Content───14*[{Web Content}]
        │         ├─WebExtensions───16*[{WebExtensions}]
        │         └─58*[{firefox}]
        ├─fwupd───4*[{fwupd}]
        ├─gdm3─┬─gdm-session-wor─┬─gdm-x-session─┬─Xorg───{Xorg}
        │      │                 │               ├─gnome-session-b
        │      │                 │               └─2*[{gdm-x-session}]
        │      │                 └─2*[{gdm-session-wor}]
        │      └─2*[{gdm3}]
        ├─gnome-keyring-d───3*[{gnome-keyring-d}]
        ├─gsd-printer───2*[{gsd-printer}]
        ├─ibus-x11───2*[{ibus-x11}]
        ├─irqbalance───{irqbalance}
        ├─2*[kerneloops]
        ├─mosquitto
        ├─networkd-dispat───{networkd-dispat}
        ├─packagekitd───2*[{packagekitd}]
        ├─polkitd───2*[{polkitd}]
        ├─pulseaudio───2*[{pulseaudio}]
        ├─python3───2*[{python3}]
        ├─rsyslogd───3*[{rsyslogd}]
        ├─rtkit-daemon───2*[{rtkit-daemon}]
        ├─snapd───14*[{snapd}]
        ├─sogoupinyinServ───4*[{sogoupinyinServ}]
        ├─sogoupinyinServ───8*[{sogoupinyinServ}]
        ├─sshd
        ├─systemd─┬─(sd-pam)
        │         ├─at-spi-bus-laun─┬─dbus-daemon
        │         │                 └─3*[{at-spi-bus-laun}]
        │         ├─at-spi2-registr───2*[{at-spi2-registr}]
        │         ├─dbus-daemon
        │         ├─dconf-service───2*[{dconf-service}]
        │         ├─evolution-addre─┬─evolution-addre───5*[{evolution-addre}]
        │         │                 └─4*[{evolution-addre}]
        │         ├─evolution-calen─┬─evolution-calen───8*[{evolution-calen}]
        │         │                 └─4*[{evolution-calen}]
        │         ├─evolution-sourc───3*[{evolution-sourc}]
        │         ├─gnome-shell-cal───5*[{gnome-shell-cal}]
        │         ├─gnome-terminal-─┬─bash───pstree
        │         │                 └─3*[{gnome-terminal-}]
        │         ├─goa-daemon───3*[{goa-daemon}]
        │         ├─goa-identity-se───3*[{goa-identity-se}]
        │         ├─gvfs-afc-volume───3*[{gvfs-afc-volume}]
        │         ├─gvfs-goa-volume───2*[{gvfs-goa-volume}]
        │         ├─gvfs-gphoto2-vo───2*[{gvfs-gphoto2-vo}]
        │         ├─gvfs-mtp-volume───2*[{gvfs-mtp-volume}]
        │         ├─gvfs-udisks2-vo───2*[{gvfs-udisks2-vo}]
        │         ├─gvfsd─┬─gvfsd-http───2*[{gvfsd-http}]
        │         │       ├─gvfsd-trash───2*[{gvfsd-trash}]
        │         │       └─2*[{gvfsd}]
        │         ├─gvfsd-fuse───5*[{gvfsd-fuse}]
        │         └─ibus-portal───2*[{ibus-portal}]
        ├─systemd-journal
        ├─systemd-logind
        ├─systemd-resolve
        ├─systemd-timesyn───{systemd-timesyn}
        ├─systemd-udevd
        ├─udisksd───4*[{udisksd}]
        ├─upowerd───2*[{upowerd}]
        ├─vmhgfs-fuse───3*[{vmhgfs-fuse}]
        ├─vmtoolsd
        ├─vmtoolsd───{vmtoolsd}
        ├─vmware-vmblock-───2*[{vmware-vmblock-}]
        ├─whoopsie───2*[{whoopsie}]
        └─wpa_supplicant
gec@ubuntu:~$ 

可以看到,最开始的系统进程叫systemd,这个进程的诞生比较特别,其身份信息在系统启动前就已经存在于系统分区之中,在系统启动时直接复制到内存。而其余的进程,从上述pstree命令的执行效果可见,都是系统初始进程的直接或间接的后代进程。

3. 进程的复刻(fork)

  • 除了系统的初始化进程之外,其他的所有进程都是通过 fork() 复刻而来的。这个所谓的复刻的过程,可以类比细胞分裂:

img
​ 细胞分裂与进程复刻

  • 一个进程复刻一个子进程的时候,会将自身几乎所有的资源复制一份,具体如下:
    • 父子进程的以下属性在创建之初完全一样:
      A) 实际UID和GID,以及有效UID和GID。
      B) 所有环境变量。
      C) 进程组ID和会话ID。
      D) 当前工作路径。
      E) 打开的文件。
      F) 信号响应函数。
      G) 整个内存空间,包括栈、堆、数据段、代码段、标准IO的缓冲区等等。
    • 而以下属性,父子进程是不一样的:
      A) 进程号PID。PID是身份证号码,哪怕亲如父子,也要区分开。
      B) 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
      C) 挂起的信号。这是所谓“悬而未决”的信号,等待着进程的响应,子进程不会继承这些信号。

3. 进程的状态

进程是动态的活动的实体,因此会有很多种运行状态:一会儿睡眠、一会儿暂停、一会儿又继续执行。下图给出Linux进程从被创建(生)到被回收(死)的全部状态,以及这些状态发生转换时的条件:

img 进程状态
  • 解析:
    • 所有进程(除了系统初始进程systemd之外),都有一个父进程。
    • 父进程通过调用fork()函数,将自身复制一份形成一个子进程。
    • 新创建的子进程拥有与父进程一样的执行代码、内存空间(父子进程的内存空间的内容是一致的,但分属不同的区域各自独立)等信息,并处于就绪态(TASK_RUNNING)。
    • 当进程退出时(不管是主动退出还是被动退出),进入僵尸态(EXIT_ZOMBIE),僵尸态下的进程无法运行,也无法被调度,但其所占据的系统资源未被释放。僵尸态是进程的必经状态,编程过程中不能避免僵尸态,但要避免进程长时间处于僵尸态。
    • 僵尸态进程要等待其父进程对其资源进程回收后,才能变成死亡态(EXIT_DEAD),死亡态的进程所有占据的系统资源可以被系统随时回收。

4. 进程创建

1668415421605

#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    int a = 0;
    // 创建子进程
    pid_t pid = fork();
    if(pid == 0)// 子进程
    {
        printf("我是子进程\n");
        a += 10;
    }
    else if(pid > 0)// 父进程id是fork的返回值
    {
        printf("我是父进程\n");
        a += 20;
    }
    else
    {
        perror("fork failed:");
    }
    printf("父子进程工作的区域\n");
    printf("a:%d\n",a);
    
    return 0;
}

image-20230904153456638

练习1:

编写一个程序,使得子进程每隔1s就打印一次apple,父进程每隔2s打印一次hello
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    int val = 0;
    // 创建子进程
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork failed:");
        return -1;
    }
    else if(pid > 0)
    {
        while(1)
        {
            printf("hello:%d,pid value : %d\n",val+=20,pid);
            sleep(2);
        }
    }
    else // 子进程
    {
        while(1)
        {
            printf("apple : %d\n",val+=10);
            sleep(1);
        }
    }
    return 0;
}

查看进程自己的PID以及父进程PID

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

pid_t getpid(void);
pid_t getppid(void);
返回值:
	成功
		getpid : 返回自己的ID号
		getppid: 返回父进程的ID号

练习2:

在子进程中打印自己与父进程的ID号,在父进程中打印自己与孩子的PID
执行程序后,打开新终端,输入ps -ef查看ID是否一致
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork failed:");
        return -1;
    }
    else if(pid > 0) // 父进程
    {
        sleep(1);
        printf("父进程:pid value : %d,[%d]是自己的ID,[%d]是老爸的ID\n",pid,getpid(),getppid());
    }
    else 
    {
        
        printf("子进程:[%d]是自己的ID,[%d]是老爸的ID\n",getpid(),getppid());
    }
    return 0;
}

孤儿进程与僵尸进程

0. 孤儿进程概念

孤儿进程:一般情况下,调用fork()函数创建的子进程,父进程如果比子进程先退出,那么这个子进程称为孤儿进程,祖先进程init就会成为该子进程的父进程,回收该子进程的资源

1. 僵尸概念

僵尸进程指的是处于僵尸态的进程,这种进程无法进行调度,但其所占用的系统资源并未被释放。僵尸态是进程生命周期的必经阶段,是无法避免的,但为了节约系统资源,应尽快清理腾出僵尸态进程所占用的内存资源。

img
​ 僵尸进程

2. 产生的原因

当一个程序的代码流程从main函数返回后,进程就结束了,但此时不能立即退出,因为还需要向其父进程汇报执行的结果和死亡的原因,又因为已无法被调度,因此进程只能以一种被动的姿态躺倒,等待其创建者(父进程)前来获取其执行结果和死亡原因。

以下代码可以查看处于僵尸态的进程:

// zombie.c
int main()
{
    // 子进程退出(变僵尸)
    if(fork() == 0)
        return 0;

    pause();
    return 0;
}

执行上述代码,并使用ps命令查看进程状态:

gec@ubuntu:~$ ./zombie
gec@ubuntu:~$ ps ajx
... ...
  8390   8422   8422   8422 pts/2      8439 Ss    1000   0:00 bash
  8422   8439   8439   8422 pts/2      8439 S+    1000   0:00 ./zombie
  8439   8440   8439   8422 pts/2      8439 Z+    1000   0:00 [zombie] <defunct>
  8406   8441   8441   8406 pts/1      8441 R+    1000   0:00 ps ajx
gec@ubuntu:~$ ps ajx

由上述结果可见,zombie父进程处于睡眠状态[S+],而其子进程[zombie]处于僵尸态[Z+]。

「课堂练习3」

使用相关命令,查看当前系统有哪些进程处于僵尸态、睡眠态等,如果没有这些状态,试试自己构建一个进程使之处于僵尸这些状态,父进程结束,子进程变僵尸。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid == 0)
    {
        exit(88);
    }
    else if(pid > 0)
    {
        while(1);
    }

    return 0;
}

3. 释放僵尸进程

上述实验中,如果父进程一直对其子进程不管不顾,那么其子进程的确会长期处于僵尸态,浪费系统资源。僵尸进程只有当以下情形之一发生时,才会释放其资源:

  • 父进程对其调用 wait()/waitpid()。
  • 父进程退出,被孤儿进程组收养。

3.0 进程回收

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
  • 主要功能:

    • 阻塞当前进程。
    • 等待其子进程退出并回收其系统资源;
  • 接口解析:

    • 如果当前进程没有子进程,则该函数立即返回。

    • 如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。

    • 子进程的退出状态(包括退出值、终止信号等)将被放入wstatus所指示的内存中,若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。

    • 功能
      WIFEXITED(status) 判断子进程是否正常退出
      WEXITSTATUS(status) 获取正常退出的子进程的退出值
      WIFSIGNALED(status) 判断子进程是否被信号杀死
      WTERMSIG(status) 获取杀死子进程的信号的值
  • 进程退出

    • exit()
    #include <stdlib.h>
    void exit(int status);
    
  • 主要功能

    • 先清洗缓冲区,再退出
  • 参数

    • 退出状态值,告诉父进程的遗言
  • 示例代码:

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

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork failed:");
        return -1;
    }
    else if(pid == 0)// 子进程
    {
        printf("[%d]: 3s后退出战斗....,退出数值为88\n",getpid());
        for(int i = 3; i > 0; i--)
        {
            fprintf(stderr,"----%d----%c",i,i==0?'\n':'\r');
            sleep(1);
        }
        // 发送遗言
        exit(88);
    }
    else // 父进程
    {
        int status;
        wait(&status);
        printf("[%d]回收子进程资源\n",getpid());
        // 判断子进程是否正常退出
        if(WIFEXITED(status))
        {
            printf("[%d]的子进程正常退出,遗言是:%d\n",getpid(),WEXITSTATUS(status));
        }
    }    
    return 0;
}

3.1 方法一:父进程直接退出

这应该不算什么方法,父进程的退出只是便于让系统中的孤儿进程组收养其孤儿进程,进而在该孤儿进程变成僵尸后由系统负责回收并释放其系统资源。

3.2 方法二:子进程等待父进程对其执行wait()/waitpid()

这是最浅显的做法,wait()/waitpid()函数有如下三个功效:

  1. 释放对应僵尸子进程的系统资源
  2. 获取对应僵尸子进程的退出状态
  3. 阻塞父进程(可选)

上述功效中的第一项即可满足我们目前的需求,比如在如上代码中,只要父进程代码做如下修改即可避免僵尸的产生:

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

int main(int argc, char const *argv[])
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork failed:");
        return -1;
    }
    else if(pid == 0)// 子进程
    {
        return -1;
    }
    else // 父进程
    {
        wait(NULL);
    return 0;
}

此办法看似简单,但并不实用,因为一般而言父进程很难预知子进程退出的时机,当父进程执行wait()的时候,要么进入漫长的等待,要么子进程尚未变僵尸,因此让父进程去时刻主动关注子进程的状态和回收资源是不切实际的,而且父进程的后代进程可能不止一个,大家任务各有长短,父进程本身也可能处于循环任务之中,因此该办法仅供参考。

作业:

1.用进程api编写一个进程扇:父进程产生一系列的子进程,每个子进程打印自己的PID然后退出,最后要求父进程也打印PID
2.用进程api编写一个进程链:父进程产生子进程后,父进程打印自己的PID,然后退出,子进程继续产生子进程,然后打出。

进程扇

image-20230904172633777

进程链

image-20230904172700084