总结:
要注意:

  1. 区分操作系统,这里是Linux是实现
  2. 区分用户态于内核态


线程有自己的 1、本地数据-线程栈,也就是独立的栈空间。2、进程内共享的全局数据。3、线程私有数据

一个进程内肯定会有一个主线程,可以有多个线程。对于内核而言,无论是进程,还是线程,到了内核里面,我们统一都叫任务(Task),由一个统一的结构 **task_struct** 进行管理。内核会有相应的链表把这些 **task_struct 串起来。**``**这也就是为什么线程是操作系统调度的最小粒度了。**

那么如何区分进程和线程呢?主要看 pidtgid

  1. 任何一个进程,如果只有主线程,那 pid 是自己,tgid【也就是target id】 是自己,group_leader 指向的还是自己。那这就是一个进程了。
  2. 但是,如果一个进程创建了其他线程,那就会有所变化了。线程有自己的 pid,**tgid 就是进程的主线程的 pid**,group_leader 指向的就是进程的主线程。这时候他就是一个线程了



进程管理 task_struct 的的结构图:
image.png

  • 进程亲缘关系维护的数据结构,是一种很有参考价值的实现方式,在内核中会多个地方出现类似的结构;
  • 进程权限中 setuid 的原理,这一点比较难理解,但是很重要,面试经常会考。
  • 在用户态,应用程序进行了至少一次函数调用。32 位和 64 的传递参数的方式稍有不同,32 位的就是用函数栈,64 位的前 6 个参数用寄存器,其他的用函数栈。
  • 在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 **pt_regs** 结构上。
  • 在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量。

image.png


task_struct

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

Linux 内核也应该先弄一个链表,将所有的 task_struct 串起来。

  1. struct list_head tasks;

任务 ID

  1. pid_t pid; // process id
  2. pid_t tgid; // thread group ID!!!
  3. struct task_struct *group_leader;

上述数据结构的作用:

  • 任务展示
  • 给任务下发指令

任何一个进程,如果只有主线程**,那 pid 是自己,tgid【也就是target id】 是自己,group_leader 指向的还是自己。

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

有了 tgid,我们就知道 tast_struct 代表的是一个进程还是代表一个线程: 【pid 是自己,tgid 是自己就可以理解做一个进程了】

信号处理

task_struct 里面关于信号处理的字段:

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

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

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

task_struct 里面有一个 struct sigpending pending。如果我们进入 struct signal_struct signal 去看的话,还有一个 struct sigpending shared_pending。*它们一个是本任务的,一个是线程组共享的。

任务状态

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

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

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

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

从定义的数值很容易看出来,flags 是通过 bitset 的方式设置的,也就是说,当前是什么状态,哪一位就置一。
image.png
TASK_RUNNING 并不是说进程正在运行,而是表示进程在时刻准备运行的状态当处于这个状态的进程获得时间片的时候,就是在运行中;如果没有获得时间片,就说明它被其他进程抢占了,在等待再次分配时间片。
**
在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态。

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

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

  • 另一种睡眠是TASK_UNINTERRUPTIBLE不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。一旦 I/O 操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程了。 kill 它呢?别忘了,kill 本身也是一个信号,既然这个状态不可被信号唤醒,kill 信号也被忽略了。除非重启电脑,没有其他办法。因此,这是一个比较危险的事情,除非程序员极其有把握,不然还是不要设置成TASK_UNINTERRUPTIBLE。

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

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

  1. #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

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

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

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

EXIT_DEAD 是进程的最终状态。

EXIT_ZOMBIEEXIT_DEAD 也可以用于 exit_state

上面的进程状态和进程的运行、调度有关系,还有其他的一些状态,我们称为标志。放在 flags 字段中,这些字段都被定义称为,以 PF 开头。我这里举几个例子。

  1. #define PF_EXITING 0x00000004
  2. #define PF_VCPU 0x00000010
  3. #define PF_FORKNOEXEC 0x00000040

PF_EXITING表示正在退出。当有这个 flag 的时候,在函数 find_alive_thread 中,找活着的线程,遇到有这个 flag 的,就直接跳过。

PF_VCPU表示进程运行在虚拟 CPU 上。在函数 account_system_time 中,统计进程的系统运行时间,如果有这个 flag,就调用 account_guest_time,按照客户机的时间进行统计。

PF_FORKNOEXEC表示 fork 完了,还没有 exec。在 _do_fork 函数里面调用 copy_process,这个时候把 flag 设置为 PF_FORKNOEXEC。当 exec 中调用了 load_elf_binary 的时候,又把这个 flag 去掉。

进程调度

进程的状态切换往往涉及调度,下面这些字段都是用于调度的。为了让你理解 task_struct 进程管理的全貌,我先在这里列一下,咱们后面会有单独的章节讲解,这里你只要大概看一下里面的注释就好了。

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

运行统计信息

进程在用户态和内核态消耗的时间、上下文切换的次数等等。

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

进程亲缘关系

任何一个进程都有父进程。所以,整个进程其实就是一棵进程树。而拥有同一父进程的所有进程都具有兄弟关系。

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

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

进程权限

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

  1. /* Objective and real subjective task credentials (COW): */
  2. const struct cred __rcu *real_cred;
  3. /* Effective (overridable) subjective task credentials (COW): */
  4. const struct cred __rcu *cred;

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

  1. struct cred {
  2. ......
  3. kuid_t uid; /* real UID of the task */
  4. kgid_t gid; /* real GID of the task */
  5. kuid_t suid; /* saved UID of the task */
  6. kgid_t sgid; /* saved GID of the task */
  7. kuid_t euid; /* effective UID of the task */
  8. kgid_t egid; /* effective GID of the task */
  9. kuid_t fsuid; /* UID for VFS ops */
  10. kgid_t fsgid; /* GID for VFS ops */
  11. ......
  12. kernel_cap_t cap_inheritable; /* caps our children can inherit */
  13. kernel_cap_t cap_permitted; /* caps we're permitted */
  14. kernel_cap_t cap_effective; /* caps we can actually use */
  15. kernel_cap_t cap_bset; /* capability bounding set */
  16. kernel_cap_t cap_ambient; /* Ambient capability set */
  17. ......
  18. } __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 也是一样的。因为谁启动的进程,就应该审核启动的用户到底有没有这个权限。

我们可以通过`chmod u+s program 命令,给这个游戏程序设置set-user-ID的标识位,把游戏的权限变成rwsr-xr-x【原本是rwxr-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 可以找到定义的权限。我这里列举几个。

  1. #define CAP_CHOWN 0
  2. #define CAP_KILL 5
  3. #define CAP_NET_BIND_SERVICE 10
  4. #define CAP_NET_RAW 13
  5. #define CAP_SYS_MODULE 16
  6. #define CAP_SYS_RAWIO 17
  7. #define CAP_SYS_BOOT 22
  8. #define CAP_SYS_TIME 25
  9. #define CAP_AUDIT_READ 37
  10. #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。

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

文件与文件系统

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

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