干货 | 一文搞懂Linux系统下进程

飞天小牛肉

共 20694字,需浏览 42分钟

 ·

2021-09-16 18:20

点击上方“君子黎”,选择“置顶/星标公众号

干货福利,第一时间送达!

1. 进程概念

所谓进程(Process),是指运行中的程序,它是程序的一个运行实例。对于同一个程序,当我们分别运行两次时候,操作系统会创建两个不同的进程。注意,程序不等同于进程,进程是正在执行中的程序以及相关资源(如程序计数器PC、打开的文件描述符、挂起的信号、处理器状态、寄存器状态、内核数据、临时数据堆栈等)的总称,而程序只是存储在某种介质上(如磁盘)的一组机器机器代码指令和数据。

对于进程与程序之间的差异,其示意图如下所示。

2. 进程数据结构

Linux操作系统为了便于管理进程,在linux/sched.h文件中定义了名为task_struct的数据结构,该结构包含了每一个运行中的具体进程所需的所有信息,比如进程优先级、进程的状态、进程PID、文件系统信息等等。对于task_struct数据结构,又称为“进程描述符(Process Descriptor)” 。Linux把每个进程(task_struct)都放在内核中的双向循环链接中,该双向循环链表又被称为“任务队列(Task List)”。

Linux 0.01版本中,该结构体(task_struct)仅有几十个成员,阅读起来十分方便。

struct task_struct {
/* these are hardcoded - don't touch */
 long state; /* -1 unrunnable, 0 runnable, >0 stopped */
 long counter;
 long priority;
 long signal;
 fn_ptr sig_restorer;
 fn_ptr sig_fn[32];
/* various fields */
 int exit_code;
 unsigned long end_code,end_data,brk,start_stack;
 long pid,father,pgrp,session,leader;
 unsigned short uid,euid,suid;
 unsigned short gid,egid,sgid;
 long alarm;
 long utime,stime,cutime,cstime,start_time;
 unsigned short used_math;
/* file system info */
 int tty;  /* -1 if no tty, so it must be signed */
 unsigned short umask;
 struct m_inode * pwd;
 struct m_inode * root;
 unsigned long close_on_exec;
 struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
 struct desc_struct ldt[3];
/* tss for this task */
 struct tss_struct tss;
};

但是到了5.4.3版本及之后,该结构体大小已经到了KB级别了,由此可见从0.01版本到5.4.3版本间新增了许多的特性和功能。

2.1 进程描述符PID

前面提到过,task_struct数据结构描述了每个进程的详细完整信息。在该数据结构中,有一个最为显眼的成员变量,即pid。它是Linux内核用来识别各进程的唯一标识,称为“进程标识值(Process Identificaion Value, PID)”。在0.01内核版本中,它是long int类型,但是之后,其类型统一适配为pid_t类型。pid_tunsigned int的一个别名。

typedef   signed int pid_t;
pid_t     pid;

从某种层面上来说,成员pid的数据类型取值范围表明了系统中允许同时运行(存在)的进程的最大数量,因为进程PID不能为负数。尽管unsigned int类型的取值范围达到了0~4294967295,但是实际情况中,基于硬件技术和资源限制等缘故,操作系统上不可能同时运行这么多数量的进程。

可通过查看/proc/sys/kernel/pid_max文件以得知当前设备环境能够支持的同时运行的最大进程数量限制。某些情况下,可能通过修改该文件配置值,以提高进程的并行数量。

2.1.1 进程资源限制

由于系统资源有限,因此,内核必须严格把控并记录每一个运行中的进程的资源(包括内存、CPU、文件句柄等)详细使用情况,并且给每一个进程一个默认限制阈值。这是保证每个进程得以在操作系统上完美运行的前提。在task_struct数据结构中,有一个数据成员signal,该成员是一个struct signal_struc类型。该数据类型中成员变量(数组)rlim用来记录当前进程的资源限制。

//记录各进程中的16个属性资源.
#define RLIM_NLIMITS  16
typedef unsigned long __kernel_ulong_t;

struct rlimit {
 __kernel_ulong_t rlim_cur;
 __kernel_ulong_t rlim_max;
};

struct signal_struct {
 struct rlimit rlim[RLIM_NLIMITS];
};
struct task_struct{
 /* Signal handlers: */
 struct signal_struct  *signal;
};

signal成员和rlim成员间的关联如下图:

signal_struct 类型声明中的rlim成员可知,该成员是一个拥有16个元素大小的数组,这意味着操作系统对每个进程有16个资源限制。对于struct rlimit数据类型,共声明了两个成员,分别是:rlim_currlim_max。其中rlim_cur是软限制(Soft Limit),rlim_max是硬限制(Hard Limit)。

软限制硬限制

1) 硬限制是指由超级用户/root设置的对用户的最大限制。该值在/etc/security/limits.conf配置文件中进行设置,将其视为上限或是天花板。

2) 软限制是内核对相应资源强制执行的值。硬限制充当软限制的上限;非特权进程可以将其软限制设置为从0到硬限制的范围内的值,并且(不可逆地)降低其硬限制。

下面是这16个特定于进程的资源限制的说明。它们定义于include/uapi/asm-generic/resource.h文件中,其中各选项的具体含义如下:

#define RLIMIT_CPU  0  // 按毫秒计算的最大CPU时间
#define RLIMIT_FSIZE 1  // 允许的最大文件长度
#define RLIMIT_DATA  2  // 数据段的最大长度 
#define RLIMIT_STACK 3  // (用户状态)栈的最大长度
#define RLIMIT_CORE  4  // core转存文件的最大长度
#define RLIMIT_RSS  5  // 常驻内存的最大尺寸(进程使用页帧的最大数目)
#define RLIMIT_NPROC 6  // 进程UID用户可以打开的进程的最大数量
#define RLIMIT_NOFILE 7  // 允许打开文件的最大数量
#define RLIMIT_MEMLOCK 8     // 不可换出页的最大数量 
#define RLIMIT_AS   9 // 进程占用的虚拟地址空间的最大尺寸
#define RLIMIT_LOCKS  10 // 文件锁的最大数目
#define RLIMIT_SIGPENDING 11 // 待决信号的最大数量
#define RLIMIT_MSGQUEUE  12 // 信息队列的最大数目
#define RLIMIT_NICE   13 // 非实时进程的优先级(和调度有关, 0-39 for nice level 19 .. -20)
#define RLIMIT_RTPRIO  14 // 最大的实时优先级
#define RLIMIT_RTTIME  15 /* 指定在不进行阻塞系统调用的情况下,根据实时调度策略调度的进程可能消耗的CPU时间的限制(以微秒为单位) */

Linux内核在/proc文件系统中,对每一个运行着的进程,都会创建一个相应的资源限制详情的文件。因此通过查看/proc/self/limits文件可得知当前进程的资源限制。查看指定进程的资源限制,只需将self换为对应的进程PID,即:/proc/PID/limits。比如查看PID为141(cat /proc/7/limits)的进程的资源限制

Linux系统提供了getrlimits()setrlimit()两个系统调用函数,它们分别用来获取、设置资源限制。

#include <sys/time.h>
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

int prlimit(pid_t pid, int resource, const struct rlimit *new_limit,
     struct rlimit *old_limit)
;

除了使用这两个系统函数外,还可以通过修改上面提到的系统配置文件/etc/security/limits.conf来动态更改资源限制。在该配置文件中,每一行均以“#<domain> <type> <item> <value>”的组成形式,描述了用户的限制。其中#<domain> 可以是用户名或组名,也可以是通配符*<type> 表明是软限制,还是硬限制。<item>表明要限制的条目(即上面16种资源限制之一);<value>则是该限制条目的值。

////vim /etc/security/limits.conf
#<domain>      <type>  <item>         <value>
#*               soft    core            0
#*               hard    rss             10000
#@student        hard    nproc           20
#@faculty        soft    nproc           20
#@faculty        hard    nproc           50
#ftp             hard    nproc           0
#@student        -       maxlogins       4

# End of file
* soft nofile 131072
* hard nofile 131072

此外,还可以通过命令行方式来进行资源限制的获取与设置。该命令是ulimit,它的使用方式如下(方括号的数字用以表明ulimit支持的选项参数)。

ulimit: usage: ulimit [-SHacdefilmnpqrstuvx] [limit]

比如查看当前系统的软限制:ulimit -S -a;查看当前系统的硬限制:ulimit -H -a。为一个变量设置指定的软限制:ulimit -S [option] [number],其中[option]是各限制的缩写,通过ulimit -a可以看到各限制条目的缩写,即下面括号中的-c、-d、-e、-f等。

[root@center-controller-7f4b9fbffc-t5wnh CenterController]# ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 255136
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) unlimited
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

为一个限制条目设置特定的硬限制:ulimit -H [option] [number]

2.2 进程运行状态

操作系统上的进程并非总是能够立刻执行,有时它需要等待内核调度的切换,以及资源的分配才能够运行。对于被加载器加载到内存并开始运行的每一个进程,都将有一个对应的状态标志,且这些状态标志是互斥的,即同一个时刻,进程只能存在一个状态(State)。这些状态可以是下图中的之一。

该状态由task_struct数据类型中的state成员表示。

struct task_struct {
 /* -1 unrunnable, 0 runnable, >0 stopped: */
 volatile long   state;
};

对于0.01版本中Linux的进程,其状态共有以下5种,它们分别是:运行状态(TASK_RUNNING)、可中断睡眠状态(TASK_INTERRUPTIBLE)、不可中断睡眠状态(TASK_UNINTERRUPTIBLE)、僵尸状态(TASK_ZOMBIE)和暂停状态(TASK_STOPPED)。这些状态定义于linux/sched.h文件中。

//linux/sched.h   0.01 version
#define TASK_RUNNING   0
#define TASK_INTERRUPTIBLE  1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE    3
#define TASK_STOPPED   4

对于TASK_RUNNINGTASK_INTERRUPTIBLETASK_UNINTERRUPTIBLETASK_ZOMBIETASK_STOPPED状态,它们的含义分别如下:

1) 可运行状态(TASK_RUNNING)

进程是可执行的。该进程可能正在执行,或在运行队列中等待被执行。该状态是进程在用户空间中执行的唯一可能状态。

2) 可中断的等待状态(TASK_INTERRUPTIBLE)

进展正在睡眠(被阻塞,或是被挂起),直到某个条件到达。唤醒进程的条件可以是:产生一个硬件中断,释放其正在等待的系统资源;亦或是传递一个信号。这时进程的状态将会切换到TASK_RUNNING

3) 不可中断的等待状态(TASK_UNINTERRUPTIBLE)

与“可中断的等待状态”类似。但是有一个不同地方是即使把信号传递到一个处于TASK_UNINTERRUPTIBLE状态中的进程,也不会改变该进程的状态。相比于“可中断的等待状态(TASK_INTERRUPTIBLE)”,使用场景相对较少,但在某些驱动程序中应用较为广泛。

4) 暂停状态(TASK_STOPPED)

进程的执行被暂停。比如当进程收到SIGSTOPSIGTSTPSIGTTIN或是SIGTTOU信号,或是在调试期间收到任何信息,都会使进程进入这种状态。

5) 僵尸状态(TASK_ZOMBIE)

进程的执行已经终止,但是其父进程还没有回收(通过wait()waitpid())该进程所占用的某些系统资源。

这5个状态之间在满足某种条件之下,能够相互地进行转换。Linux 5.4.3版本的中,进程的状态已经添加了很多,如下所示:

/* Used in tsk->state: */
#define TASK_RUNNING  0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED  0x0004
#define __TASK_TRACED  0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD   0x0010
#define EXIT_ZOMBIE   0x0020
#define EXIT_TRACE   (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED   0x0040
#define TASK_DEAD   0x0080
#define TASK_WAKEKILL  0x0100
#define TASK_WAKING   0x0200
#define TASK_NOLOAD   0x0400
#define TASK_NEW   0x0800
#define TASK_STATE_MAX  0x1000

这里对新增的__TASK_TRACED、和TASK_DEAD状态进行简要的补充说明。

1) 跟踪状态 (__TASK_TRACED)

被其他进程跟踪的进程。如通过ptrace 对调试程序进行跟踪。__TASK_TRACED本身不是进程状态,它主要用于区分常规的进程。

2) 僵尸撤销状态(TASK_DEAD)

对于TASK_DEAD状态,《深入Linux内核》一书中有说道,“它是指wait()系统调用已发出,而进程完全从系统中移除之前的状态。只有多个线程同时对同一个进程发出wait()调用时候,该状态才有意义。”

2.2.1 进程状态的缩写标志

在2.3节里详细描述了Linux系统上进程的可呈现状态和转换。通常使用top来查看进程列表时候,其状态列都仅显示一个缩写标志,比如RSD等,下面将一一列出通常进程状态的缩写。

R是可执行状态(TASK_RUNNING)的状态标志;S是可中断的睡眠状态(TASK_INTERRUPTIBLE)状态标志;D是不可中断睡眠状态(TASK_UNINTERRUPTIBLE)的状态标志;T是暂停状态(TASK_STOPPED)或跟踪状态(TASK_TRACED)的标志;Z是僵尸进程(TASK_ZOMBIE)的状态标志。

2.3 查找指定进程PID

对于运行中的进程,可以使用以下几种方式来快速查看其进程的PID。

(1) ps假如当前系统上面运行着进程A,那么使用ps命令查看该进程的PID时,结合管道、grep命令方式,即: ps -axu|grep A ,即可得到进程A的PID。

可以看到该进程的PID是4007。

(2) pidof除了使用ps来查看进程的PID外,还可以使用“pidof 进程名”的方式来查看。如下:

(3) pgrep pgrep命令是使用通过grep命令管道传输的ps命令的快捷方式。它使用名称或定义的模式搜索特进程的所有匹配项。其命令语法为:pgrep <options> <pattern>。常用的选项参数如下:

 -d, --delimiter <string>  specify output delimiter
 -l, --list-name           list PID and process name
 -a, --list-full           list PID and full command line
 . . . . . . //省略若干选项参数
 -v, --inverse             negates the matching
 -w, --lightweight         list all TID
 -c, --count               count of matching processes

比如查看kubelet进程的PID,并且列出该PID对应的进程名,则使用pgrep -l kubelet

附:可以使用命令"pstree -p PID"、"ps -ejH"、"ps -aux --forest"或"ps axjf"以树(tree)的形式打印进程。

2.4 查找指定PID的线程

获取操作系统上有关线程的信息,可以使用“ps -eLf”和“ps axms”命令。若想查看某个进程下的线程,则可使用“ps -T -p PID”,其中参数-T可以开启线程;或使用“top -H -p PID”方式。也可以直接使用pstree -p PID命令以树形方式打印指定PID下的所有线程列表信息,不过该方式不会列出线程名。

2.5 kill指定PID进程

kill掉一个正在运行的进程,最常规的方法是先ps找出该进程的PID,然后再使用kill()命令向该进程PID发送信号以达到终止进程的目的。其实,有一个命令可以帮我们省去ps查找进程PID的过程。这个命令是pkill(此外,还有killall),该命令也是与kill命令一起使用ps命令的快捷方式。pkill命令用于根据进程PID名称向指定进程发送信号。

pkill命令的语法格式是:pkill [options] <pattern>。该命令常见的选项参数有:

-<sig>, --signal <sig>    signal to send (either number or name)
 -e, --echo                display what is killed
 -c, --count               count of matching processes
 . . . . . . //省略
 -f, --full                use full process name to match
 -g, --pgroup <PGID,...>   match listed process group IDs
 -P, --parent <PPID,...>   match only child processes of the given parent

比如有一个进程A,现想要使用STGSTOP信号停止该进程,则可以:pkill -19 A。使用kill -l命令可以查看所有信号及对应的序号。

[root@node1 ~]# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

3. 僵尸进程

对于僵尸进程,维基上面是这样描述的:

在Unix和类Unix计算机操作系统上,僵尸进程或失效进程是已完成执行(通过exit()系统调用)但在进程表(Process Table)中仍有条目的进程:它是处于“终止状态”的进程。这发生在子进程中,其中仍然需要该条目以允许父进程读取其子进程的退出状态;一旦通过wait()waitpid()系统调用读取退出状态,则僵尸进程的条目将从进程表中删除,并被收割。子进程从资源表中删除之前总数首先变成僵尸进程。大多数情况下,在正常的系统操作中,僵尸进程会立即被它们的父进程wait,然后被系统收割。长时间处于僵尸状态(TASK_ZOMBIE)的进程通常是一个错误,且会导致资源泄露。

进程终止后,有些信息对于父进程和内核是很有用的,比如进程PID、进程退出状态、进程运行的CPU时间等。不允许类UNIX内核在进程一终止后就丢弃包含在进程描述符字段中的数据。只有父进程发出来wait()waitpid()等系统调用之后才可以。

注:进程使用exit()系统调用终止时,分配给该进程的所有内存和资源都将被释放。但是进程表中的条目仍然可用,比如PID、进程表项等。

3.1 僵尸进程产生

从上面对僵尸进程的定义可知它发生在子进程中。其具体的产生过程是:父进程调用fork()创建子进程后,子进程一直运行直到其终止。此时,它将立即从内存中移除,但是其PID仍然保留在内存中(尽管PID占用的空间并不大)。这时子进程的状态成为EXIT_ZOMBIE,并向其父进程发送SIGCHLD信号。正常情况下,该进程的父进程此时会调用waitwaitpid系统函数来获取子进程的退出状态及相关信息,并于wait()/waitpid()之后,僵尸进程将完全彻底地从内存中移除。但如果因为代码编写缺陷或是其他的一些因素影响,导致父进程未调用wait()/waitpid()系统函数,则该进程将一直成为僵尸进程。

僵尸进程存在于其终止一直到父进程调用wait()/waitpid()系统函数这个时间段期间。

僵尸进程的创建与终止过程可参考下图。

为了更深一步理解僵尸进程的产生、终止过程,现编写一个demo来进行演示说明。在该示例中,子进程打印PID之后便结束,而父进程也没有调用wait()或是waitpid()来回收子进程的系统资源,直到睡眠2min之后,整个进程结束,此时进程的所有资源将交接给init进程来进行回收。

// file: zombie.c, gcc zombie.c -o zombie
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
     pid_t pid = 0;
     pid = fork();
     if(0 == pid){
         //打印进程PID之后,调用exit()系统函数结束.
        printf("Child PID is %ld\n", (long)getpid());
        exit(0);
     }else if(0 < pid){
        puts("Parent process . . .");
        sleep(120);
        //不回收子进程的资源.
        //wait(NULL);
     }else{
        perror("fork");
        exit(-1);
     }

       return 0;
}

可看到(在2min时间段内)zombie进程处于僵尸状态(EXIT_ZOMBIE)。而该僵尸进程(PID466910)的父进程(PPID466909)则处于“可中断的睡眠状态”(左下窗口2)。

3.2 僵尸进程危害

尽管僵尸进程不占用任何资源,但是它们会保留其进程PID,即进程描述符(PID)将永久占据着RAM。若有大量的僵尸进程存在,那么所有可用的进程ID都将被独占,会导致其他进程没有可用的ID。此外,在较重负载下,会给系统带来重大的问题。

3.3 杀死僵尸进程

僵尸进程可以通过使用kill命令向其父进程发送SIGCHLD信号来终止。该信号将通知父进程调用wait()系统函数来回收该僵尸进程。即:kill -s SIGCHLD PID,这里PID是父进程的ID

若要找僵尸进程的父进程,可以使用命令:ps -o ppid = -p PID,这里的PID是子进程的进程ID。当然也可以使用ps -aux|grep 进程名的方式。

4. 进程与线程

4.1 线程概念

进程是具有一定独立功能的程序在数据集上的动态执行过程,它是操作系统中资源分配和调度的独立单元,是应用程序的载体,每个进程都有自己独立的内存空间,每个进程内存地址彼此隔离。进程一般由程序(Program)、数据集指令(Data Collection)和进程控制块(Process Control Blok, PCB)组成。其中程序用来描述要完成的功能,是控制进程执行的指令集;而数据集是程序执行过程中所需要的数据和工作区域;进程控制块则包含进程的描述信息,并且控制信息是进程存在的唯一标志。

4.2 线程表示形式

线程是进程执行操作的最小单位,也是处理器调度和分派的基本单位,它是进程的一个实体。一个进程可以有一个或多个线程,各线程间共享其所在进程的内存空间。一个标准线程是由线程ID、程序计算机PC、寄存器和堆栈组成。

4.3 两者区别

一个线程只能属于一个进程,但是一个进程可以有多个线程,且至少有一个线程。线程是进程中最小的执行单位,是调度的最小单位。而进程是资源的基本单位。进程和线程都可以并非,但是创建、销毁一个进程比创建、销毁一个线程的开销要大很多,并且进程是拥有资源的独立单位,而一个线程却仅拥有很少的系统资源。最后,进程和线程在内核中都是由task_struct数据类型表示。

- END -


浏览 49
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报