关于进程:进程最后的遗言

8次阅读

共计 6855 个字符,预计需要花费 18 分钟才能阅读完成。

过程最初的遗嘱

前言

在本篇文章当中次要给大家介绍父子过程之间的关系,以及他们之间的交互以及可能造成的状态,帮忙大家深刻了解父子过程之间的关系,以及他们之间的交互。

僵尸过程和孤儿过程

僵尸过程

在 Unix 操作系统和类 Unix 操作系统当中,当子过程退出的时候,父过程能够从子过程当中获取子过程的退出信息,因而在 类 Unix 操作系统当中只有父过程通过 wait 零碎调用读取子过程的退出状态信息之后,子过程才会齐全退出。那么子过程在程序执行实现之后(调用 _exit 零碎调用之后),到父过程执行 wait 零碎调用获取子过程退出状态信息之前,这一段时间的过程的状态是僵尸过程的状态。

正式定义:在 Unix 或者类 Unix 操作系统当中,僵尸过程就是哪些曾经实现程序的执行(实现_exit 零碎调用退出了程序),然而在内核当中还有属于这个过程的过程表项。这个表项的作用次要是让父过程读取子过程的退出状态信息 (exit status)。

在后文当中咱们有一个例子详细分析这个退出状态的相干信息。一旦父过程通过 wait 零碎调用读取完子过程的 exit statis 信息之后,僵尸过程的过程表项就会从过程表(process table)当中被移除,这个过程就算是彻底沦亡了(reaped)。

如果零碎当中有很多处于僵尸状态的过程而父过程又没有应用 wait 零碎调用去失去子过程的退出状态,那么零碎当中就会有很多内存没有被开释,这就会导致资源泄露。

上面是一个僵尸过程的例子,对应的代码名称为 Z.c:

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


int main() {printf("parent pid = %d\n", getpid());
  if(fork()) {while(1);
  }
  printf("child process pid = %d\n", getpid());
  return 0;
}

下面 C 语言对应的 python 代码如下:

import os

if __name__ == "__main__":
    print(f"parent pid = {os.getpid()}")
    pid = os.fork()
    if pid != 0:
        # parent process will never exit
        while True:
            pass
    # child process will exit
    print(f"child process pid = {os.getpid()}")

当初执行下面的程序,失去的后果如下所示:

从上图当中咱们能够看到父过程始终在进行死循环的操作,而子过程退出了程序,而当初应该处于僵尸过程状态。而咱们通过 ps 命令失去的过程状态的后果,依据过程号失去子过程的状态为 Z+,这个状态就示意这个过程就是一个僵尸过程。咱们在这里再简要谈一下命令 ps 对过程的状态的各种示意:

STAT 当中字母的含意表:

条目 含意
D 示意不可能被中断的睡眠操作,比如说 IO 操作
I 内核当中的闲暇线程
R 正在执行或者处于就绪队列当中的过程
S 能够被中断的睡眠,个别是期待某个事件触发
T 被其余的过程发送的信号给停下来了
t 被调试或者 tracing 中
Z 示意这个过程是一个僵尸过程
< 示意高优先级
N 示意低优先级
L 有页面被所在内存当中,也就是说这个页面不会被操作系统换出道对换区当中
s 示意这个过程是一个 session leader
l 是一个多线程程序
+ 示意在前台过程组当中

大家能够依据上表当中的内容对应一下程序的状态,就发现子过程目前处于僵尸状态。

孤儿过程

孤儿过程:当一个过程还在执行,然而他的父过程曾经退出了,那么这个过程就变成了一个孤儿过程,而后他会被 init 过程(过程 ID=1)” 收养 ”,而后 init 过程会调用 wait 零碎调用回收这个过程的资源。

上面是一个孤儿过程的例子,咱们能够看看子过程的父过程的输入是什么:

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

int main() {if(fork()) {sleep(1);
    // 父过程退出
    exit(0);
  }
  while(1) {printf("pid = %d parent pid = %d\n", getpid(), getppid());
    sleep(1);
  }
  return 0;
}

对应的 python 代码如下:

import os
import time


if __name__ == "__main__":
    pid = os.fork()

    if pid == 0:
        while True:
            print(f"pid = {os.getpid()} parent pid = {os.getppid()}")
            time.sleep(1)

程序执行后果如下所示:

能够看到子过程的父过程产生了变动,当父过程退出之后,子过程的父过程变成了 init 过程,过程号等于 1。

wait 零碎调用

waitpid 及其参数剖析

在前文当中咱们次要谈到了两种比拟不一样的过程,其中更是次要谈到了 wait 零碎调用对于僵尸过程的重要性。在 linux 当中与 wait 相干的次要有上面两个零碎调用:

pid_t waitpid(pid_t pid, int *wstatus, int options);
pid_t wait(int *wstatus);

其中 wait 零碎调用是 waitpid 零碎调用的一个特例,咱们首先解释一下 waipit 零碎调用。下面两个零碎调用次要是用于期待子过程的状态变动的,并且从子过程当中取出额定的状态信息(status information)。只有当子过程的状态发生变化了 wait 零碎调用能力返回,次要有以下几种状态:

  • 子过程完结了。
  • 子过程被别的过程发送的信号进行运行了(SIGSTOP 和 SIGTSTP 能够让一个过程被挂起进行执行)。
  • 进行运行的过程被信号唤醒继续执行了(SIGCONT 能够唤醒过程继续执行)。

当有子过程呈现下面的状态的时候 wait 或者 waitpid 零碎调用会马上返回,否则 wait 或者 waitpid 零碎调用就会统一阻塞。waitpid 的几个参数:

  • pid:

    • pid < -1 示意期待任何过程组 -pid 当中的该过程的子过程。
    • pid == -1 示意期待任何一个子过程。
    • pid == 0 示意期待子过程,这些子过程的过程组号(process group id)等于这个过程(调用 waitpid 函数的这个过程)的过程号。
    • pid > 0 示意期待过程号等于 pid 的子过程。
  • options:

    • WNOHANG:如果 options 等于这个值的话,示意如果还没有子过程完结执行就立刻返回,不进行期待。
    • WUNTRACED:如果子过程被其余过程发送的信号 stop 了,wait 函数也返回。
    • WCONTINUED:如果子过程被其余过程发送的信号(SIGCONT)复原执行了,wait 函数也返回。
    • 依据下面的剖析,waitpid(-1, &wstatus, 0) == wait(&wstatus)
  • wstatus:是咱们传入给 wait 或者 waitpid 函数的一个指针,零碎调用会将子过程的很多状态信息放入到 wstatus 指针指向的数据当中,咱们能够应用上面的一些宏去判断一些信息。

    • WIFEXITED(wstatus):如果子过程失常退出(应用 exit、_exit 或者间接从 main 函数返回)这行代码就返回为 true。
    • WEXITSTATUS(wstatus):这个宏次要是返回程序退出的退出码,对于退出码的内容,能够参考这篇文章 Shell 揭秘——程序退出状态码。
    • WIFSIGNALED(wstatus):示意子过程是否是其余过程发送信号导致程序退出的。
    • WTERMSIG(wstatus):如果子过程是其余过程发送信号导致程序退出的话,咱们能够应用这个宏去失去具体的信号值。
    • WCOREDUMP(wstatus):示意子过程是否产生 core dump 而后退出的。
    • WIFSTOPPED(wstatus):当子过程接管到了一个其余过程发送的信号导致程序被挂起,这个宏就返回 true。
    • WSTOPSIG(wstatus):返回挂起信号的具体的信号值。
    • WIFCONTINUED(wstatus): 返回 true 如果子过程接管到了一个 SIGCONT 信号恢复程序的执行。

示例阐明

上面是一个简直综合了下面所有的信息的一个例子,咱们认真看看这个程序的输入:


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


int main(int argc, char *argv[])
{
    pid_t cpid, w;
    int wstatus;

    cpid = fork();
    if (cpid == -1) {perror("fork");
        exit(EXIT_FAILURE);
    }

    if (cpid == 0) {            /* Code executed by child */
        printf("Child PID is %jd\n", (intmax_t) getpid());
        if (argc == 1)
            pause();   // 子过程会在这里期待接管信号 直到信号的处理函数返回                  /* Wait for signals */
        _exit(atoi(argv[1]));

    } else {                    /* Code executed by parent */
        do {w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED);
            if (w == -1) {perror("waitpid");
                exit(EXIT_FAILURE);// EXIT_FAILURE 是一个宏 等于 1
            }
                        // 程序是否是失常退出
            if (WIFEXITED(wstatus)) {printf("exited, status=%d\n", WEXITSTATUS(wstatus));
            } else if (WIFSIGNALED(wstatus)) { // 是否是被信号杀死
                printf("killed by signal %d\n", WTERMSIG(wstatus));
            } else if (WIFSTOPPED(wstatus)) { // 是否被 stop 
                printf("stopped by signal %d\n", WSTOPSIG(wstatus));
            } else if (WIFCONTINUED(wstatus)) { // 是否处于 stop 状态再被唤醒
                printf("continued\n");
            }
          // 判断程序是否退出 失常退出和因为信号退出 如果程序是退出了 父过程就退出 while 循环
        } while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus));
        exit(EXIT_SUCCESS); // EXIT_SUCCESS 是一个宏 等于 0
    }
}

在上图的例子当中,咱们在下面的终端先执行 wait.out 程序,而后在上面一个终端咱们首先发送一个 SIGSTOP 信号给子过程,先让过程停下来,而后在发送一个 SIGCONT 信号让过程继续执行,最初发送了一个 SIGKILL 信号让子过程退出,能够看到咱们下面提到的宏操作都一一失效了,在父过程当中信号和退出状态都一一承受了。

咱们再来演示一个 coredump 的例子:

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

int main() {if(fork()) {
    int s;
    wait(&s);
    if(WCOREDUMP(s)) {printf("core dump true\n");
    }
  }else{int a = *(int*)NULL;
  }
}

子线程解援用 NULL 会造成 segmentation fault (core dump),而后父过程接管子过程的退出状态,而后对状态进行判断,是否是 coredump 导致的退出:

咱们在父过程对子过程的退出码进行了判断,如果子过程退出的起因是 core dump 的话就进行打印输出,而在下面的程序输入当中咱们看到了程序进行了输入,因而父过程能够判断子过程是因为 core dump 而退出程序的。

从子过程获取系统资源信息

出了下面所谈到的等在子过程退出的办法之外,咱们还有能够获取子过程执行时候的状态信息,比如说运行时占用的最大的内存空间,过程有多少次上下文切换等等。次要有上面两个零碎调用:

pid_t wait3(int *wstatus, int options,
                   struct rusage *rusage);
pid_t wait4(pid_t pid, int *wstatus, int options,
                   struct rusage *rusage);

其中 3 和 4 示意对应的函数的参数的个数。在下面的两个函数当中有一个比拟重要的数据类型 struct rusage,咱们看一下这个构造体的内容和对应字段的含意:

struct rusage {
  struct timeval ru_utime; /* user CPU time used */ // 程序在用户态的时候应用了多少的 CPU 工夫
  struct timeval ru_stime; /* system CPU time used */ // 程序在内核态的时候应用了多少 CPU 工夫
  long   ru_maxrss;        /* maximum resident set size */ // 应用的内存的峰值 单位 kb
  long   ru_ixrss;         /* integral shared memory size */ // 临时没有应用
  long   ru_idrss;         /* integral unshared data size */ // 临时没有应用
  long   ru_isrss;         /* integral unshared stack size */ // 临时没有应用
  long   ru_minflt;        /* page reclaims (soft page faults) */ // 没有 IO 操作时候 page fault 的次数
  long   ru_majflt;        /* page faults (hard page faults) */ // 有 IO 操作的时候 page fault 的次数
  long   ru_nswap;         /* swaps */ // 临时没有应用
  long   ru_inblock;       /* block input operations */ // 文件系统写操作次数
  long   ru_oublock;       /* block output operations */ // 文件系统读操作次数
  long   ru_msgsnd;        /* IPC messages sent */// 临时没有应用
  long   ru_msgrcv;        /* IPC messages received */ // 临时没有应用
  long   ru_nsignals;      /* signals received */ // 临时没有应用
  long   ru_nvcsw;         /* voluntary context switches */ // 被动上下文切换的次数
  long   ru_nivcsw;        /* involuntary context switches */ // 非被动上下文切换的次数
};

上面咱们用一个例子看看是如何从子过程当中获取子过程执行时候的一些具体数据信息的:

上面是子过程的代码,咱们在 fork 之后应用 execv 加载上面的程序:

#include <stdio.h>


int main() {printf("hello world\n");
  return 0;
}

父过程代码:

#include <sys/time.h>
#include <stdio.h>
#include <sys/resource.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char* argv[]) {if(fork() != 0) {
    struct rusage usage; // 定义一个统计资源的构造体
    int pid = wait4(-1, NULL, 0, &usage); // 将这个构造体的地址传入 好让内核能讲对应的信息存在指针所指向的中央
    // 打印内存应用的峰值
    printf("pid = %d memory usage peek = %ldkb\n", pid, usage.ru_maxrss);
  }else {execv("./getrusage.out", argv);
  }
  return 0;
}

下面程序执行后果如下图所示:

能够看出咱们失去了内存应用的峰值,其实咱们还能够应用 time 命令去查看一个过程执行时候的这些数据值。

从下面的后果能够看到应用 time 命令失去的后果和咱们本人应用程序失去的后果是一样的,这也从侧面验证了咱们的程序。如果要达到下面的成果的话,须要留神应用相对地址命令,因为 time 是 shell 的保留字。

总结

在本篇文章当中次要给大家具体介绍了僵尸过程、孤儿过程、父过程从子过程获取过程退出信息,以及他们造成的起因,并且应用理论的例子进行了验证,这一部分常识练习比拟严密,心愿大家有所播种!


以上就是本篇文章的所有内容了,我是 LeHung,咱们下期再见!!!更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHu…

关注公众号: 一无是处的钻研僧 ,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

正文完
 0