内核中如何管理线程呢?

有的进程只有一个线程,有个进程有多个线程,但是都需要内核CPU来分配时间去干活

如何进行调度呢?

首先说,公司内有多个项目组,项目组对应进程,进程多是好事,但是进程之间有不同的开发模式

但从操作系统角度来说,这些项目需要有一个统一的项目管理体系

就好比无论是单体应用模式,还是微服务模式,都是根据客户的需求去定制的,都需要统一的协调资源等

同样在Linux中,无论是进程还是线程,都是统一的叫做Task,交给task_struct进行管理

我们先说下这个task_struct的结构

图片

我们设想一下,Linux的任务管理应该干什么呢?

首先需要一个统一的项目管理列表,所以Linux维护了一个链表,进行串起来

struct list_head tasks

然后是任务ID

每个任务都有一个ID,作为这个任务的唯一表示,所以标识位如下

pid_t pid

pid_t tgid

struct task_struct *group_leader

但是作为唯一标识,为何有这么多的标识位呢?

逐步展开来讲,我们需要进行任务展示,每个小组都是啥情况,如果将所有线程进行平铺展示,会让用户觉着复杂和疑惑,复杂在于列表如此长,疑惑在于线程很多不是自己创建的

其次是给任务下达指令,如果一个客户给项目组提了一个新的需求,客户可能觉着项目已经完成,可以终止,那么操作系统给谁发指令,是整个项目组吧,Linux也一样,我们学习命令行的时候,可以通过kill给线程发信号,通知进程退出,发给其中一个线程,就不能只退出一个线程,而是整个进程

所以在内核中,任务需要加以区分,其中pid是process id,tgid thread group Id

一个任务,如果只有主线程,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,

信号处理函数使用用户态的信号栈

还有最后的,任务的状态,也就是线程的状态

在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

上面的定义中,每一位,对应着的2进制的一位

现在是什么状态,哪一位就置为一

图片

TASK_RUNNING是第一种状态,不是说进程正在进行,而是进程时刻准备运行的状态

当这个状态的进程获得时间片的时候,就是运行中,没有则说明需要等待再次分配时间片

在Linux中有两种睡眠状态

一种是TASK_INTERRUPTIBLE,可中断的睡眠状态,一种浅睡眠的状态,方便对应的唤醒操作

一种是TASK_UNINTERRUPTIBLE,不可中断的睡眠状态,不可被唤醒,只能死等,也不可被唤醒,KILL都唤不醒

后来有了新的进程睡眠状态,TASK_KILLABLE,可以终止新的睡眠状态,在TASK_UNINTERRUPTIBLE的基础上可以唤醒致命信号

TASK_WAKEKILL用于接收到致命信号时候唤醒进程,

TASK_STOPPED在收到SIGSTOP,SIGTTN,SIGTSTP或者SIGTTOU信号之后进入该状态

TASK_TRACED表示进程debugger等进程监视

进程结束的时候,进入的是EXIT_ZOMBIE状态,如果父进程还没有使用wait()等系统调用,就一直等待下去了

EXIT_DEAD也可以用于exit_state

上面的进程状态和进程运行 调度都有关系,还有其他状态,称为标志,放在flags字段中,都定义为了宏,以PF开头

#define PF_EXITING    0x00000004

#define PF_VCPU      0x00000010

#define PF_FORKNOEXEC    0x00000040

PF_EXITING,表示正在退出,方便统计活着的线程的时候,有这个flag,就直接跳过

PF_VCPU,在虚拟cpu上运行

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;

这次,我们总结一下task_struct的结构图

图片

接下来就是项目的运行情况

如何了解一个员工的工作状态呢?进程的运行过程中,会有一些统计量,具体可以看下面的列表,会有进程在用户态和内核态消耗的时间,上下文切换的次数

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的时候一般都一致,但是如果bash上使用GDB进行debug的时候,GDB是read_parent,bash是parent

进程权限

进程权限的定义基本如下

/* 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是我操作的对象

操作是一些动作的实施,实施的时候,需要审核权限,其中real_cread是谁能操作我

cread是我能操作谁

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 user id group id 一般不审核这两个

euid egid effective user group id 起作用的,操作内存,信号量的时候,比较这个用户和组是否有权限

fsuid fsgid 文件操作审核权限

一般来说,fsuid euid uid都是一致的,fsgid egid gid都是一样的

图片

我们想使用一个游戏,但是游戏属于B,所以B需要给用户A权限

所以设置了执行权限 rwxr-xr-x

A有了用户权限,uid euid fsuid都是A

但是A需要保存数据的时候,发现数据是保存在其他文件中的,这就导致没法写入到文件,白玩了

如何操作呢?可以通过chmod u+s program命令,设置set-user-ID标识位,将权限设置位rwsr-xr-x

用户A启动的时候,利用set-user-id标识,将文件所有者ID,改为了用户B

利用setuid来改变权限

最后是内存管理

每个进程都有自己的独立虚拟内存空间,有一个数据结构来表示,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的原理,比较难以理解

图片

最后,我们说一下内核栈的几个变量

struct thread_info thread_info

void *stack

用户态的函数栈

用户态的程序执行是一个程序调用另一个程序,函数调用通过栈进行的,我们大致讲了函数站的原理

函数调用很简单,看一下汇编语言的代码,就是指令跳转,从代码一个地方调转到别的地方,参数和返回地址如何传递过去呢?

比如用户态中调用,A调用B B调用C C调用D 然后返回 C B A

后进先出的过程,这就是基于栈的调用方式

图片

32位操作系统的情况,在CPU中 ESP是栈顶探针寄存器,入栈操作Push和出栈操作Pop指令

自动调整ESP的值,另外有一个寄存器EBP,栈基地址指针寄存器,指向当前栈帧的最底部

A调用B A栈中包含A函数的局部变量 以及调用B的时候传给B的参数

返回A的地址,这个地址应该入栈,就是形成了A的栈帧,然后是B的栈帧部分

先保存的是A栈帧的栈底位置,就是EBP,因为B中获取函数A的参数,就是通过这个指针获取的,最后保存的就是B的局部变量

B返回的时候,返回值保存在EAX寄存器中,从栈中弹出返回地址,指令跳转回去,参数也从栈中弹出,继续执行A

对于64位系统,模式多少不一样,因为64位系统的寄存器的数目比较多,rax保存函数调用的返回结果,栈顶指针寄存器变成了rbp,指向当前栈帧的其实位置

参数传递利用了rdi rsi rdx rcx r8 r9 6个寄存器,传递函数调用的时候6个函数,超过6个,需要放入栈中

图片

上面我们通过系统调用,从进程的内存空间到内核中了,内核中有各种各样函数调用,基本上也是栈的调用机制

上面的变量stack,就是内核栈,派上了用场

Linux给每个task都分配了内核栈,32位系统和64位不一样

32位上,一个PAGE_SIZE是4K,左移一位就是乘以2,8K

64位上,在PAGE_SIZE基础上左移两位,就是16K,起始地址必须是8192的整数倍

内核栈结构如下

图片

最低位置,是一个thread_info结构,结构是task_struct结构的补充,和体系结构相关的,都放在thread_info中

内核中,将thread_info和stack放在一起,形成一个union

也就是我们的进程数据结构

内核栈的最高地址栈,存放的是另一个结构pt_regs,定义如下,32位和64位的定义不一样

#ifdef __i386__

struct pt_regs {

unsigned long bx;

unsigned long cx;

unsigned long dx;

unsigned long si;

unsigned long di;

unsigned long bp;

unsigned long ax;

unsigned long ds;

unsigned long es;

unsigned long fs;

unsigned long gs;

unsigned long orig_ax;

unsigned long ip;

unsigned long cs;

unsigned long flags;

unsigned long sp;

unsigned long ss;

};

#else

struct pt_regs {

unsigned long r15;

unsigned long r14;

unsigned long r13;

unsigned long r12;

unsigned long bp;

unsigned long bx;

unsigned long r11;

unsigned long r10;

unsigned long r9;

unsigned long r8;

unsigned long ax;

unsigned long cx;

unsigned long dx;

unsigned long si;

unsigned long di;

unsigned long orig_ax;

unsigned long ip;

unsigned long cs;

unsigned long flags;

unsigned long sp;

unsigned long ss;

/* top of stack page */

};

#endif

从用户态到内核态的时候,第一件事,就是将用户态的CPU上下文保存到这个结构中,然后从内核系统调用返回的时候,才能让进程在刚刚的地方继续运行下去

如果有一个task_struct的stack指针在手,可以通过下面的函数找到线程内核栈

static inline void *task_stack_page(const struct task_struct *task)

{

return task->stack;

}

task_struct可以获取到对应的pt_regs

/*

* TOP_OF_KERNEL_STACK_PADDING reserves 8 bytes on top of the ring0 stack.

* This is necessary to guarantee that the entire “struct pt_regs”

* is accessible even if the CPU haven’t stored the SS/ESP registers

* on the stack (interrupt gate does not save these registers

* when switching to the same priv ring).

* Therefore beware: accessing the ss/esp fields of the

* “struct pt_regs” is possible, but they may contain the

* completely wrong values.

*/

#define task_pt_regs(task) \

({                  \

unsigned long __ptr = (unsigned long)task_stack_page(task);  \

__ptr += THREAD_SIZE – TOP_OF_KERNEL_STACK_PADDING;    \

((struct pt_regs *)__ptr) – 1;          \

})

task_struct找到内核栈的开始位置,加上了Thread_SIZE到了最后的位置,然后转换为struct_pt_regs,减一,减少一位pt_regs位置,到了结构的首地址.

里面有一个TOP_OF_KERNEL_STACK_PADDING

其中,32位上是8,其他是0,为何,因为压栈有两种情况,CPU利用ring进行区分,从而Linux可以区分内核态和用户态

第一种情况,涉及用户态到内核态的系统调用来说,设计权限的变化,压栈保存SS ESP寄存器,占用8个byte

不涉及权限的变化,不会压栈这8个byte,使得两个情况不兼容,没有压栈访问必然会保存,所以预留在此

64位上改为了定长的

如果有task_struct在手,可以轻松获得内核栈和内核寄存器

内核栈找task_struct

在某个CPU上执行的进程,想知道自己的task_struct在哪,怎么办?

交给了thread_info

struct thread_info {

struct task_struct  *task;    /* main task structure */

__u32      flags;    /* low level flags */

__u32      status;    /* thread synchronous flags */

__u32      cpu;    /* current CPU */

mm_segment_t    addr_limit;

unsigned int    sig_on_uaccess_error:1;

unsigned int    uaccess_err:1;  /* uaccess failed */

};

其中的task指针指向了task_struct

thread_info的位置就是内核栈的最高位置,减去THREAD_SIZE,就到了thread_info的起始位置

最后就只剩下一个flags

如何获取到当前运行中的task_struct内?

CPU运行的task_struct不通过thread_info获取到的,直接放在Per CPU中了,Per CPU就是为了每个CPU构造一个变量的副本,多个CPU各自操作自己的副本,互不干涉,当前进程变量的current_task就是声明为了Per CPU

图片 总结一下处理

在用户态,应用程序至少进行了一次函数调用.32位和64的传递参数方式不同,32位的是函数栈,64位用前6个参数寄存器,其他用函数栈

内核态,32位和64位都使用内核栈

内核态中,32位和64位的内核栈和task_struct的关系不同,32位主要靠thread_info,64位主要靠Per-CPU变量

发表评论

邮箱地址不会被公开。 必填项已用*标注