linux操作系统:进程数据结构,项目多了就需要项目管理系统

引入

linux内核是如何管理线程和进程的呢?

  • 有的进程只有一个线程,有的进程有多个线程,它们都需要内核分配CPU来干活。可是CPU个数是有限的,应该怎么管理,怎么调度呢?
  • 类比项目管理中,无论是大项目还是小项目,从项目经理的角度来看,这些都是任务
  • 同样在linux里面无论是进程,还是线程,到了内核里面,统一叫做任务(Task),由一个统一的结构task_struct进行管理

在这里插入图片描述
接下来,我们沿着项目管理体系的思路,设想一下,linux的任务管理都应该干些什么?

  • 首先,所有项目的执行都应该有个项目列表吧,所以Linux内核也应该先弄一个链表,将所有的task_struct串起来:
struct list_head tasks;
  • 接下来,我们来看每一个任务应该包含哪些字段

任务ID

  • 每一个任务都应该有一个ID,作为这个任务的唯一标识。到时候排期、下发任务等,都按照这个ID来,就不会产生歧义。
  • task_struct里面涉及任务ID的,有下面几个:
pid_t pid;    // process id
pid_t tgid;   // thread group ID
struct task_struct *group_leader; 
  • 问题:既然是任务ID,有一个就足以做唯一标识了,这个怎么这么麻烦?这是因为,上面的进程和线程到了内核里面,统一变成了任务,这就会带来两个问题:
    • 第一个问题:任务展示,进程、线程之间的关系应该怎么表示呢?
    • 第二个问题:给任务下指令,比如,我们kill命令时,怎么给某个线程,或者还在进程,还是进程组来发命令呢?
  • 所以在内核中,虽然它们都是任务,但是应该加以区分。
  • 任何一个线程,如果只有主线程,那pid是自己,tgid也是自己,group_leader指向的也是自己
  • 但是,如果一个进程创建了其他线程,那就会变化了。线程有自己的pid,tgid就是进程的主线程的pid,group_leader指向的就是进程的主线程。
  • 有了tgid之后,我们就知道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;
  • 这里定义了哪些信号被阻塞暂不处理(blocked),哪些信号尚待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)。处理的结果可以是忽略,可以是结束等等
  • 信号处理函数默认使用用户态的函数栈,当然也可以开辟新的栈专门用于信号处理,这就是sas_ss_xxx这三个变量的作用

任务状态

  • 在 task_struct 里面,涉及任务状态的是下面这几个变量:
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
 int exit_state;
 unsigned int flags;
  • 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
  • 从定义的数值很容易看出来,flags 是通过 bitset 的方式设置的也就是说,当前是什么状态,哪一位就置一。

在这里插入图片描述

  • TASK_RUNNING 并不是说进程正在运行,而是表示进程在时刻准备运行的状态。当处于这个状态的进程获得时间片的时候,就是在运行中;如果没有获得时间片,就说明它被其他进程抢占了,需要等待再次分配时间片。
  • 在运行中的进程,一旦要进程一些IO操作,需要等待IO完毕,这个时候会释放CPU,进入睡眠状态
  • 在Linux中,有三种睡眠状态
    • 一种是是TASK_INTERRUPTIBLE可中断的睡眠状态
      • 这是一种浅睡眠状态,也就是,虽然在睡眠,等待IO完成,但是这个时候一个信号来的时候,进程还是要被唤醒。
      • 只不过唤醒后,不是继续刚才的操作,而是进行信号处理。
    • 一种是TASK_UNINTERRUPTIBLE不可中断的睡眠状态
      • 这是深度睡眠状态,不可被信号唤醒(包括kill信号),只能死等IO操作完成
      • 一旦IO操作因为特殊原因不能完成,这个时候,谁也叫不醒这个线程了,除非重启信号
      • 因此,除非非常必要,不要设置成TASK_UNINTERRUPTIBLE
    • 为此,有了一种新的进程睡眠状态,TASK_KILLABLE可以终止的新睡眠状态
      • 其作用原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号
      • 从定义可以看出,TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,而TASK_KILLABLE 相当于这两位都设置了。
#define TASK_KILLABLE           (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
  • TASK_STOPPED 是在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态。
  • TASK_TRACED 表示进程被 debugger 等进程监视,进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。
  • 一旦一个进程要结束,先进入的是EXIT_ZOMEBIE状态,但是这个时候它的父进程还没有使用wait()等系统调用来获取它的终止进程,此时进程成为僵死进程
  • EXIT_DEAD 是进程的最终状态
  • EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。

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

#define PF_EXITING 0x00000004
#define PF_VCPU 0x00000010
#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 去掉。

进程调度

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

// 是否在运行队列上
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;


运行统计信息

在进程的运行过程中,会有一些统计量,如下:

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 指向其父进程。当它终止时,必须向它的父进程发送信号。
    • 通常情况下,real_parent 和 parent 是一样的,但是也会有另外的情况存在。例如,
    • bash 创建一个进程,那进程的 parent 和 real_parent 就都是 bash。如果在 bash 上使用 GDB 来 debug 一个进程,这个时候 GDB 是 real_parent,bash 是这个进程的 parent。
  • children 表示链表的头部。链表中的所有元素都是它的子进程。
  • sibling 用于把当前进程插入到兄弟链表中。
    在这里插入图片描述

进程权限

事实上,所谓的权限,就是我能操纵谁,谁能操纵我。

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

操作”,就是一个对象对另一个对象进行某些动作。当动作要实施的时候,就要审核权限,当两边的权限匹配上了,就可以实施操作。其中,

  • real_cred 就是说明谁能操作我这个进程
  • cred 就是说明我这个进程能够操作谁。
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu         *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu         *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还有另一个机制capablities

  • 原来控制进程的权限,要么是高权限的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
  • 对于普通用户运行的进程,当有这个权限的时候,就能做这些操作;没有的时候,就不能做,这样粒度要小很多

内存管理

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

struct mm_struct                *mm;
struct mm_struct                *active_mm;

文件和文件系统

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

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

在程序执行过程中,一旦调用到系统调用,就需要进入内核继续执行。那如何将用户态的执行和内核态的执行串起来呢?

这就要用到下面两个成员变量:

struct thread_info		thread_info;
void  *stack;

在这里插入图片描述

进程状态

在liunx中,每个进程每个时刻都是有状态的,可能的状态共有6个。

  • 可运行状态(TASK_RUNNING(task_running), 简称R): 系统立刻要或者正在CPU上运行,不过运行的时机是不确定的,这由进程调度器决定。
  • 可中断的睡眠状态(TAST_INTERRUNPTIBLE, 简称S):当进程正在等待某个事件(比如网络连接/信号量)到来,此时进程进入对应事件的等待状态中。当事件发生时,对应的等待队列中的一个或者多个进程会被唤醒。
  • 不可中断的睡眠状态(TAST_UNINTERRUNPTIBLE, 简称D):与上面的唯一区别在于这种状态的进程不会对任何信号响应。 这样的进程一般是在等待某个特殊事件,比如同步IOC操作 【???这个怎么醒的???】
  • 暂停状态或者跟踪状态(TASK_STOPPED或者TASK_TRACED,简称T):
    • 向不处于D状态的进程发送SIGSTOP信号,该进程会进入暂停状态,直到另一个进程向它发送SIGCONT信号,这个进程会转为R状态。
    • 处于跟踪状态的进程也会暂停,但是向它发送SIGCONT信号,这个进程不会恢复。比如我们使用GDB(调试进程)调试程序时,对应的进程运行到断点处就会停下来,这个时候,该进程就处于跟踪状态,向这个进程发送SIGCONT信号,这个进程不会恢复,只有当调试进程进行响应的系统调用或者退出之后,被跟踪的进程才能恢复。
  • 僵尸状态(TASK_DEAD-EXIT_ZOMBIE,简称Z):处于该状态的进程即将结束,该进程占用的绝大多数资源也被回收,不够还有一些信息比如退出码没有被删除。之所以保留这些信息,是因为该进程的父进程可能需要它们。由于此时进程主题已经被删除而只留下一个空壳,所以叫做僵尸进程。
  • 退出状态(TASK_DEAD-EXIT_DEAD,简称X):处于退出状态的进程会被结束掉,它占用的系统资源也会被操作系统自动回收。可能进入X状态的原因如下
    • 显式的让该进程的父进程忽略掉SIGCHLD信号(当一个进程结束的时候,内核会给父进程发送SIGCHLD)
    • 父子进程已经分离:分离后的子进程将不再共享父进程的代码,而是加载一个权限的程序。

简单来说,进程的状态只会在运行和非运行状态之间转换
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值