• 【Linux】进程数据结构


    【Linux】进程数据结构

    在 Linux 中,无论是进程,还是线程,到了内核里面,我们统一都叫 任务(Task),由一个统一的结构 task_struct 进行管理。

    image-20221106193531932

    Linux 的任务管理都应该干些啥?

    首先,所有执行的项目应该有个项目列表吧,所以 Linux 内核也应该先弄一个 链表,将所有的 task_struct 串起来。

    struct list_head		tasks;
    
    • 1

    接下来,我们来看每一个任务都应该包含哪些字段。

    任务 ID

    每一个任务都应该有一个 ID,作为这个任务的唯一标识。到时候排期啊、下发任务啊等等,都按 ID 来,就不会产生歧义。task_struct 里面涉及任务 ID 的,有下面几个:

    pid_t pid;
    pid_t tgid;
    struct task_struct *group_leader; 
    
    • 1
    • 2
    • 3

    为什么 ID 需要这么多字段,有一个就足以做唯一标识了,这个怎么看起来这么麻烦?

    因为,上面的进程和线程到了内核这里,统一变成了任务,这就带来两个问题。

    第一个问题是,任务展示

    啥是任务展示呢?这么说吧,你作为老板,想了解的肯定是,公司都接了哪些项目,每个项目多少营收。什么项目执行是不是分了小组,每个小组是啥情况,这些细节,项目经理没必要全都展示给你看。

    前面我们学习命令行的时候,知道 ps 命令可以展示出所有的进程。但是如果你是这个命令的实现者,到了内核,按照上面的任务列表把这些命令都显示出来,把所有的线程全都平摊开来显示给用户。用户肯定觉得既复杂又困惑。复杂在于,列表这么长;困惑在于,里面出现了很多并不是自己创建的线程。

    第二个问题是,给任务下发指令

    如果客户突然给项目组提个新的需求,比如说,有的客户觉得项目已经完成,可以终止;再比如说,有的客户觉得项目做到一半没必要再进行下去了,可以中止,这时候应该给谁发指令?当然应该给整个项目组,而不是某个小组。我们不能让客户看到,不同的小组口径不一致。这就好比说,中止项目的指令到达一个小组,这个小组很开心就去休息了,同一个项目组的其他小组还干的热火朝天的。

    Linux 也一样,前面我们学习命令行的时候,知道可以通过 kill 来给进程发信号,通知进程退出。如果发给了其中一个线程,我们就不能只退出这个线程,而是应该退出整个进程。当然,有时候,我们希望只给某个线程发信号。

    所以在内核中,它们虽然都是任务,但是应该加以区分。其中,pidprocess idtgid thread group ID

    任何一个进程,如果只有主线程,那 pid 是自己,tgid 是自己,group_leader 指向的还是自己。

    但是,如果一个进程创建了其他线程,那就会有所变化了。线程有自己的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程。

    好了,有了 tgid,我们就知道 tast_struct 代表的是一个进程还是代表一个线程了。

    信号处理

    task_struct 中关于信号处理的字段:

    /* Signal handlers: */
    struct signal_struct				*signal;
    struct sighand_struct				*sighand;
    sigset_t										blocked;
    sigset_t										real_blocked;
    sigset_t										saved_sigmask;
    struct sigpending						pending;
    unsigned long								sas_ss_sp;
    size_t											sas_ss_size;
    unsigned int								sas_ss_flags;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里定义了哪些信号被阻塞暂不处理(blocked),哪些信号尚等待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)。处理的结果可以是忽略,可以是结束进程等等。

    信号处理函数默认使用用户态的函数栈,当然也可以开辟新的栈专门用于信号处理,这就是 sas_ss_xxx 这三个变量的作用。

    任务状态

    在 task_struct 里面,涉及任务状态的是下面这几个变量:

     volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
     int exit_state;
     unsigned int flags;
    
    • 1
    • 2
    • 3

    state(状态)可以取的值定义在 include/linux/sched.h 头文件中。

    /* Used in tsk->state: */
    #define TASK_RUNNING                    0
    #define TASK_INTERRUPTIBLE              1
    #define TASK_UNINTERRUPTIBLE            2
    #define __TASK_STOPPED                  4
    #define __TASK_TRACED                   8
    /* Used in tsk->exit_state: */
    #define EXIT_DEAD                       16
    #define EXIT_ZOMBIE                     32
    #define EXIT_TRACE                      (EXIT_ZOMBIE | EXIT_DEAD)
    /* Used in tsk->state again: */
    #define TASK_DEAD                       64
    #define TASK_WAKEKILL                   128
    #define TASK_WAKING                     256
    #define TASK_PARKED                     512
    #define TASK_NOLOAD                     1024
    #define TASK_NEW                        2048
    #define TASK_STATE_MAX                  4096
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    从定义的数值很容易看出来,flags 是通过 bitset 的方式设置的也就是说,当前是什么状态,哪一位就置一

    image-20221106194957085

    TASK_RUNNING 并不是说进程正在运行,而是 表示进程在时刻准备运行的状态。当处于这个状态的进程获得时间片的时候,就是在运行中;如果没有获得时间片,就说明它被其他进程抢占了,在等待再次分配时间片。

    在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态。

    在 Linux 中,有两种睡眠状态。

    TASK_INTERRUPTIBLE

    一种是 TASK_INTERRUPTIBLE可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等待 I/O 完成,但是这个时候一个信号来的时候,进程还是要被唤醒。只不过唤醒后,不是继续刚才的操作,而是进行信号处理。当然程序员可以根据自己的意愿,来写信号处理函数,例如收到某些信号,就放弃等待这个 I/O 操作完成,直接退出,也可也收到某些信息,继续等待。

    TASK_UNINTERRUPTIBLE

    另一种睡眠是 TASK_UNINTERRUPTIBLE不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。一旦 I/O 操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程了。你可能会说,我 kill 它呢?别忘了,kill 本身也是一个信号,既然这个状态不可被信号唤醒,kill 信号也被忽略了。除非重启电脑,没有其他办法。

    因此,这其实是一个比较危险的事情,除非程序员极其有把握,不然还是不要设置成 TASK_UNINTERRUPTIBLE。

    TASK_KILLABLE

    于是,我们就有了一种新的进程睡眠状态,TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号

    从定义可以看出,TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,而 TASK_KILLABLE 相当于这两位都设置了。

    #define TASK_KILLABLE           (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
    
    • 1
    TASK_STOPPED

    TASK_STOPPED 是在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态。

    TASK_TRACED

    TASK_TRACED 表示进程被 debugger 等进程监视,进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。

    EXIT_DEAD

    一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了 僵尸进程

    EXIT_DEAD 是进程的最终状态。EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。

    进程调度

    进程的状态切换往往涉及调度,下面这些字段都是用于调度的。

    // 是否在运行队列上
    int				on_rq;
    // 优先级
    int				prio;
    int				static_prio;
    int				normal_prio;
    unsigned int			rt_priority;
    // 调度器类
    const struct sched_class	*sched_class;
    // 调度实体
    struct sched_entity		se;
    struct sched_rt_entity		rt;
    struct sched_dl_entity		dl;
    // 调度策略
    unsigned int			policy;
    // 可以使用哪些 CPU
    int				nr_cpus_allowed;
    cpumask_t			cpus_allowed;
    struct sched_info		sched_info;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    运行统计信息

    在进程的运行过程中,会有一些统计量,具体你可以看下面的列表。这里面有进程在用户态和内核态消耗的时间、上下文切换的次数等等。

    作为项目经理,你肯定需要了解项目的运行情况。例如,有的员工很长时间都在做一个任务,这个时候你就需要特别关注一下;再如,有的员工的琐碎任务太多,这会大大影响他的工作效率。

    u64				utime;// 用户态消耗的 CPU 时间
    u64				stime;// 内核态消耗的 CPU 时间
    unsigned long			nvcsw;// 自愿 (voluntary) 上下文切换计数
    unsigned long			nivcsw;// 非自愿 (involuntary) 上下文切换计数
    u64				start_time;// 进程启动时间,不包含睡眠时间
    u64				real_start_time;// 进程启动时间,包含睡眠时间
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    进程亲缘关系

    从我们之前讲的创建进程的过程,可以看出,任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而 拥有同一父进程的所有进程都具有兄弟关系

    struct task_struct __rcu *real_parent; /* real parent process */
    struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
    struct list_head children;      /* list of my children */
    struct list_head sibling;       /* linkage in my parent's children list */
    
    • 1
    • 2
    • 3
    • 4
    • parent 指向其父进程。当它终止时,必须向它的父进程发送信号。
    • children 表示链表的头部。链表中的所有元素都是它的子进程。
    • sibling 用于把当前进程插入到兄弟链表中。

    image-20221106203623916

    通常情况下,real_parent 和 parent 是一样的,但是也会有另外的情况存在。例如,bash 创建一个进程,那进程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 real_parent,bash 是这个进程的 parent。

    进程权限

    在 Linux 里面,对于进程权限的定义如下:

    /* Objective and real subjective task credentials (COW): */
    // 谁能操作我
    const struct cred __rcu         *real_cred;
    /* Effective (overridable) subjective task credentials (COW): */
    // 我能操作谁
    const struct cred __rcu         *cred;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个结构的注释里,有两个名词比较拗口,Objective 和 Subjective。事实上,所谓的权限,就是我能操纵谁,谁能操纵我。

    “谁能操作我”,很显然,这个时候我就是被操作的对象,就是 Objective,那个想操作我的就是 Subjective。“我能操作谁”,这个时候我就是 Subjective,那个要被我操作的就是 Objectvie。

    “操作”,就是一个对象对另一个对象进行某些动作。当动作要实施的时候,就要审核权限,当两边的权限匹配上了,就可以实施操作。其中,real_cred 就是说明谁能操作我这个进程,而 cred 就是说明我这个进程能够操作谁。

    这里 cred 的定义如下:

    struct cred {
    ......
            kuid_t          uid;            /* real UID of the task */
            kgid_t          gid;            /* real GID of the task */
            kuid_t          suid;           /* saved UID of the task */
            kgid_t          sgid;           /* saved GID of the task */
            kuid_t          euid;           /* effective UID of the task */
            kgid_t          egid;           /* effective GID of the task */
            kuid_t          fsuid;          /* UID for VFS ops */
            kgid_t          fsgid;          /* GID for VFS ops */
    ......
            kernel_cap_t    cap_inheritable; /* caps our children can inherit */
            kernel_cap_t    cap_permitted;  /* caps we're permitted */
            kernel_cap_t    cap_effective;  /* caps we can actually use */
            kernel_cap_t    cap_bset;       /* capability bounding set */
            kernel_cap_t    cap_ambient;    /* Ambient capability set */
    ......
    } __randomize_layout;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    从这里的定义可以看出,大部分是关于用户和用户所属的用户组信息

    第一个是 uidgid,注释是 real user/group id。一般情况下,谁启动的进程,就是谁的 ID。但是权限审核的时候,往往不比较这两个,也就是说不大起作用。

    第二个是 euidegid,注释是 effective user/group id。一看这个名字,就知道这个是起“作用”的。当这个进程要操作消息队列、共享内存、信号量等对象的时候,其实就是在比较这个用户和组是否有权限。

    第三个是 fsuidfsgid,也就是 filesystem user/group id。这个是对文件操作会审核的权限。

    一般说来,fsuid、euid,和 uid 是一样的,fsgid、egid,和 gid 也是一样的。因为谁启动的进程,就应该审核启动的用户到底有没有这个权限。但是也有特殊的情况。

    image-20221106204858447

    例如,用户 A 想玩一个游戏,这个游戏的程序是用户 B 安装的。游戏这个程序文件的权限为 rwxr–r–。A 是没有权限运行这个程序的,因而用户 B 要给用户 A 权限才行。用户 B 说没问题,都是朋友嘛,于是用户 B 就给这个程序设定了所有的用户都能执行的权限 rwxr-xr-x,说兄弟你玩吧。

    于是,用户 A 就获得了运行这个游戏的权限。当游戏运行起来之后,游戏进程的 uid、euid、fsuid 都是用户 A。看起来没有问题,玩的很开心。

    用户 A 好不容易通过一关,想保存通关数据的时候,发现坏了,这个游戏的玩家数据是保存在另一个文件里面的。这个文件权限 rw-------,只给用户 B 开了写入权限,而游戏进程的 euid 和 fsuid 都是用户 A,当然写不进去了。完了,这一局白玩儿了。

    那怎么解决这个问题呢?我们可以通过 chmod u+s program 命令,给这个游戏程序设置 set-user-ID 的标识位,把游戏的权限变成 rwsr-xr-x。这个时候,用户 A 再启动这个游戏的时候,创建的进程 uid 当然还是用户 A,但是 euid 和 fsuid 就不是用户 A 了,因为看到了 set-user-id 标识,就改为文件的所有者的 ID,也就是说,euid 和 fsuid 都改成用户 B 了,这样就能够将通关结果保存下来。

    在 Linux 里面,一个进程可以随时通过 setuid 设置用户 ID,所以,游戏程序的用户 B 的 ID 还会保存在一个地方,这就是 suid 和 sgid,也就是 saved uidsave gid。这样就可以很方便地使用 setuid,通过设置 uid 或者 suid 来改变权限。

    内存管理

    每个进程都有自己独立的虚拟内存空间,这需要有一个数据结构来表示,就是 mm_struct

    struct mm_struct                *mm;
    struct mm_struct                *active_mm;
    
    • 1
    • 2

    文件与文件系统

    每个进程有一个文件系统的数据结构,还有一个打开文件的数据结构。

    /* Filesystem information: */
    struct fs_struct                *fs;
    /* Open file information: */
    struct files_struct             *files;
    
    • 1
    • 2
    • 3
    • 4

    总结

    image-20221106204739066

  • 相关阅读:
    Docker 搭建个人博客(solo)
    javascript函数式编程初探——什么是函数式编程?
    vue总结(一)
    leetcode做题笔记2342. 数位和相等数对的最大和
    搭建STM32F407的SPI-Flash(基于STM32CubeMX)
    [附源码]计算机毕业设计JAVA超市订单管理系统
    Pytorch深度学习——循环神经网络基础 (07)
    Ubuntu下vscode dotNet downloading的问题(Cmake代码高亮)
    【Redis使用】一年多来redis使用笔记md文档,第(2)篇:命令和数据库操作
    达梦数据库V8(启动数据库实例服务)
  • 原文地址:https://blog.csdn.net/weixin_41960890/article/details/127720962