13 | 进程数据结构(中):项目多了就需要项目管理系统

上一节我们讲了,task_struct这个结构非常长。由此我们可以看出,Linux内核的任务管理是非常复杂的。上一节,我们只是讲了一部分,今天我们接着来解析剩下的部分。

运行统计信息

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

那如何才能知道这些员工的工作情况呢?在进程的运行过程中,会有一些统计量,具体你可以看下面的列表。这里面有进程在用户态和内核态消耗的时间、上下文切换的次数等等。

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

进程亲缘关系

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

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 */
  • parent指向其父进程。当它终止时,必须向它的父进程发送信号。

  • children表示链表的头部。链表中的所有元素都是它的子进程。

  • sibling用于把当前进程插入到兄弟链表中。

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

这个结构的注释里,有两个名词比较拗口,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;

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

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

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

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

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

但是也有特殊的情况。

例如,用户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 uid和save gid。这样就可以很方便地使用setuid,通过设置uid或者suid来改变权限。

除了以用户和用户组控制权限,Linux还有另一个机制就是capabilities

原来控制进程的权限,要么是高权限的root用户,要么是一般权限的普通用户,这时候的问题是,root用户权限太大,而普通用户权限太小。有时候一个普通用户想做一点高权限的事情,必须给他整个root的权限。这个太不安全了。

于是,我们引入新的机制capabilities,用位图表示权限,在capability.h可以找到定义的权限。我这里列举几个。

#define CAP_CHOWN            0
#define CAP_KILL             5
#define CAP_NET_BIND_SERVICE 10
#define CAP_NET_RAW          13
#define CAP_SYS_MODULE       16
#define CAP_SYS_RAWIO        17
#define CAP_SYS_BOOT         22
#define CAP_SYS_TIME         25
#define CAP_AUDIT_READ          37
#define CAP_LAST_CAP         CAP_AUDIT_READ

对于普通用户运行的进程,当有这个权限的时候,就能做这些操作;没有的时候,就不能做,这样粒度要小很多。

cap_permitted表示进程能够使用的权限。但是真正起作用的是cap_effective。cap_permitted中可以包含cap_effective中没有的权限。一个进程可以在必要的时候,放弃自己的某些权限,这样更加安全。假设自己因为代码漏洞被攻破了,但是如果啥也干不了,就没办法进一步突破。

cap_inheritable表示当可执行文件的扩展属性设置了inheritable位时,调用exec执行该程序会继承调用者的inheritable集合,并将其加入到permitted集合。但在非root用户下执行exec时,通常不会保留inheritable集合,但是往往又是非root用户,才想保留权限,所以非常鸡肋。

cap_bset,也就是capability bounding set,是系统中所有进程允许保留的权限。如果这个集合中不存在某个权限,那么系统中的所有进程都没有这个权限。即使以超级用户权限执行的进程,也是一样的。

这样有很多好处。例如,系统启动以后,将加载内核模块的权限去掉,那所有进程都不能加载内核模块。这样,即便这台机器被攻破,也做不了太多有害的事情。

cap_ambient是比较新加入内核的,就是为了解决cap_inheritable鸡肋的状况,也就是,非root用户进程使用exec执行一个程序的时候,如何保留权限的问题。当执行exec的时候,cap_ambient会被添加到cap_permitted中,同时设置到cap_effective中。

内存管理

每个进程都有自己独立的虚拟内存空间,这需要有一个数据结构来表示,就是mm_struct。这个我们在内存管理那一节详细讲述。这里你先有个印象。

struct mm_struct                *mm;
struct mm_struct                *active_mm;

文件与文件系统

每个进程有一个文件系统的数据结构,还有一个打开文件的数据结构。这个我们放到文件系统那一节详细讲述。

/* Filesystem information: */
struct fs_struct                *fs;
/* Open file information: */
struct files_struct             *files;

总结时刻

这一节,我们终于把进程管理复杂的数据结构基本讲完了,请你重点记住以下两点:

  • 进程亲缘关系维护的数据结构,是一种很有参考价值的实现方式,在内核中会多个地方出现类似的结构;

  • 进程权限中setuid的原理,这一点比较难理解,但是很重要,面试经常会考。

你可以对着下面这张图,看看自己是否真的理解了,进程树是如何组织的,以及如何控制进程的权限的。

课堂练习

通过这一节的学习,你会发现,一个进程的运行竟然要保存这么多信息,这些信息都可以通过命令行取出来,所以今天的练习题就是,对于一个正在运行的进程,通过命令行找到上述进程运行的所有信息。

欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。

精选留言

  • 石维康

    2019-04-24 10:42:11

    文中说“如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 real_parent,bash 是这个进程的 parent。”
    根据copy_process里对PT_PTRACED标志位处理的流程来看,应该是 bash 是 real_parent,GDB 是这个进程的 parent 吧?
    作者回复

    谢谢,果真弄反了

    2019-04-25 20:23:35

  • garlic

    2019-07-03 17:59:00

    进程的相关信息可以通过ps 获取, 依赖关系通过pstree获取,文件相关通过lsof, fuser,capabilities相关通过capsh,getcap获取, 学习笔记https://garlicspace.com/2019/07/03/获取进程信息相关命令/
    作者回复

    2019-09-03 16:04:40

  • why

    2019-04-24 17:36:13

    - 运行统计信息, 包含用户/内核态运行时间; 上/下文切换次数; 启动时间等;
    - 进程亲缘关系
    - 拥有同一父进程的所有进程具有兄弟关系
    - 包含: 指向 parent; 指向 real_parent; 子进程双向链表头结点; 兄弟进程双向链表头结点
    - parent 指向的父进程接收进程结束信号
    - real_parent 和 parent 通常一样; 但在 bash 中用 GDB 调试程序时, GDB 是 real_parent, bash 是 parent
    - 进程权限, 包含 real_cred 指针(谁能操作我); cred 指针(我能操作谁)
    - cred 结构体中标明多组用户和用户组 id
    - uid/gid(哪个用户的进程启动我)
    - euid/egid(按照哪个用户审核权限, 操作消息队列, 共享内存等)
    - fsuid/fsgid(文件操作时审核)
    - 这三组 id 一般一样
    - 通过 chmod u+s program, 给程序设置 set-user-id 标识位, 运行时程序将进程 euid/fsuid 改为程序文件所有者 id
    - suid/sgid 可以用来保存 id, 进程可以通过 setuid 更改 uid
    - capability 机制, 以细粒度赋予普通用户部分高权限 (capability.h 列出了权限)
    - cap_permitted 表示进程的权限
    - cap_effective 实际起作用的权限, cap_permitted 范围可大于 cap_effective
    - cap_inheritable 若权限可被继承, 在 exec 执行时继承的权限集合, 并加入 cap_permitted 中(但非 root 用户不会保留 cap_inheritable 集合)
    - cap_bset 所有进程保留的权限(限制只用一次的功能)
    - cap_ambient exec 时, 并入 cap_permitted 和 cap_effective 中
    - 内存管理: mm_struct
    - 文件与文件系统: 打开的文件, 文件系统相关数据结构
  • WL

    2019-04-30 08:55:18

    请问一下老师这句代码中的, __rcu是什么意思? 起到的作用是啥? 在网上好像没搜到.
    const struct cred __rcu *real_cred;

    作者回复

    RCU(Read-Copy Update),是 Linux 中比较重要的一种同步机制。顾名思义就是“读,拷贝更新”,再直白点是“随意读,但更新数据的时候,需要先复制一份副本,在副本上完成修改,再一次性地替换旧数据”。这是 Linux 内核实现的一种针对“读多写少”的共享数据的同步机制。

    2019-09-05 17:48:05

  • 逍觉迷遥

    2019-04-24 13:21:57

    ①这节内容解决了关于上节task_struct为什么要用链表结构,是为了维护多个task之间的关系。一个task节点的parent指针指向其父进程task,children指针指向子进程所有task的头部,然后又靠 sibling指针来维护统一级兄弟task!

    ②setUid是一个权限的特殊标志位,带有这个标志位可以对文件执行等同root的权限!比如,Linux下修改密码的指令passwd,我们ls去查看时发现其权限就有s标志,这个就是之所以能修改密码的原因。如果去掉这个权限,再以普通用户身份去修改密码则会提示没有权限!这个例子和老师的玩游戏可以说是异曲同工,但它更具有普遍性,大家也更熟悉!

    ③capabilities听了感觉云里雾里还是一知半解,老师着重讲了概念,但还是不知道怎么用???希望老师结合实际使用例子讲一下就更好了!!!
    作者回复

    capabilities在容器那一节会用到

    2019-04-25 20:37:33

  • manatee

    2019-04-24 09:40:12

    通过/proc/pid/status可以看到进程的所属的各种id
    作者回复

    建议大家多看/proc/pid下的相关信息

    2019-04-25 20:35:38

  • wumin

    2019-05-21 08:03:06

    作业能给出具体的命令吗?
    作者回复

    其实一搜就能搜索到的

    2019-09-04 23:15:24

  • czh

    2019-11-25 18:02:10

    如果task的数据结构就是为了打印出来看我觉得还是比较容易的,关键还是要站在linux创造的角度,理解为什么要这样设计数据结构,这需要对整个linux系统进行整体的学习和分析,继续加油,多看两遍!
  • geek

    2021-03-30 23:07:09

    https://www.liquidweb.com/kb/how-do-i-set-up-setuid-setgid-and-sticky-bits-on-linux/
    https://linux-audit.com/linux-capabilities-101/
  • 佳佳大魔王

    2020-07-26 00:13:41

    各个进程的进程亲缘关系所依赖的数据结构不就是B+树嘛,这样子就一下子把数据结构和操作系统连起来了,果然哪里都离不开数据结构
  • 辉煌码农

    2019-09-20 08:23:56

    进程亲缘关系的图超赞
  • 勤劳的小胖子-libo

    2019-05-12 20:00:19

    通过passwd来试验setUid很方便,谢谢一起学习的同志
    作者回复

    2019-09-05 11:28:51

  • Linuxer

    2019-04-24 08:59:06

    u64 real_start_time;// 进程启动时间,包含睡眠时间 感觉不好理解是不是启动时长?
    作者回复

    CLOCK_MONOTONIC是monotonic time,随着进程的运行不断加,而CLOCK_REALTIME是wall time,墙上的时间,实际时间,系统管理员可以调整的时间

    2019-04-25 20:35:09

  • 涛子

    2022-07-29 11:35:48

    这节开始全是定义啊,人开始晕了
  • cv0cv0

    2022-03-15 11:22:00

    Windows的权限完全搞不明白。
  • Ken

    2021-12-15 08:23:06

    刘老师,您好。进程的树状图是用什么工具画的?其他的图也画得非常棒,想学一下。谢谢老师。
  • 夏晨

    2021-10-10 21:10:58

    每个进程都有parent sibling这些字段么
  • 忘忧草的约定

    2021-06-30 13:28:18

    老师请问suid和uid是什么关系?uid的文件存储叫做suid?
  • Geek_b8928e

    2020-03-22 21:48:35

    task成员包括
    1 任务标识
    2 任务状态
    3 运行统计
    4 调度信息

    5 亲缘关系
    6 权限信息

    7 信号处理
    8 内存管理
    9 文件相关
    10 内核栈
  • 起而行

    2020-02-12 20:04:45

    day13,
    进程的亲缘关系很有趣,也突然想了解pstree的实现原理。
    权限管理的确需要理解原理才能够操作,我在linux中有时会用chomd 777,现在看起来是个不太安全的操作。
    学习linux最好的地方在于,通过命令行阅读内核,是非常好的反馈