Linux内核原理-进程入门

进程

进程是处于执行期的程序,进程不仅仅是一段可执行程序的代码,通常进程还包括其他资源,比如打开的文件,挂起的信号,内核内部的数据结构,处理器状态,内存地址空间,或多个执行线程,存放全局变量的数据段等。线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器,进程炸,和一组进程寄存器。内核调度的对象是线程,而不是进程。传统的Unix系统中一个进程只包含一个线程。对于linux而言,线程只不过是一种特殊的进程罢了。在linux系统中通过fork()来复制现有进程的资源和创建一个新的进程。随后调用exec这组函数创建自己的地址空间最后载入要运行的程序,在linux中fork其实是clone系统调用来实现。程序执行完毕后使用exit退出执行,父进程调用wait或waitpid来等待子进程结束,并回收其资源。

进程描述符及任务结构

在linux内核中,使用了一个task_struct结构体来描述进程信息,并且通过双向循环链表将所有的进程连接起来。一个task_struct描述了进程打开文件信息,状态信息,地址空间信息,挂起的信号,进程的状态等等。

进程之间的关联如下图:
Linux内核原理-进程入门_第1张图片

进程描述符的分配

在linux中进程描述符task_struct是通过slab分配器来进行分配的,这样可以达到对象复用和缓存着色的效果。task_struct这个结构体本身就很大,这个结构体在内核中会不断的进行分配和释放,这样很不高效,使用了salb分配器后只需要进行分配释放是没有开销的,slab释放了task_struct只是放在其对象池中,并没有真正释放。然后在2.6内核之前,task_struct的分配和释放不是通过slab分配器,而是直接放在内核栈的栈底,这样就可以通过栈顶指针很快的计算出task_struct的地址,因为在内核中task_struct是最经常要访问的数据结构,所以需要一个机制很快的获取当前进程的task_struct结构体的地址,在x86这种体系结构中,因为寄存器的缺乏没有独立的寄存器用来存放task_struct的地址,所以就通过这种方式来快速计算,在PowerPC系列的计算机中则是使用一个寄存器来存放task_struct的地址。在2.6内核后为了避免动态内存分配和释放所带来的资源消耗,采用了slab分配器来管理task_struct结构体,但是为了快速计算task_struct的地址,linux在栈低存放了一个thread_info结构 在这个结构中第一个元素就是当前的task_struct地址,这样就可以很快的计算出task_strcut的地址,并且task_struct交由slab分配器来管理,可以避免资源消耗。
获得thread_info地址的方法

struct thread_info {
    unsigned long flags;        /* low level flags */
    int preempt_count;      /* 0 => preemptable, <0 => BUG */
    struct task_struct *task;   /* main task structure */
    mm_segment_t addr_limit;    /* thread address space */
    struct exec_domain *exec_domain;/* execution domain */
    __u32 cpu;          /* current CPU */
    unsigned long thr_ptr;      /* TLS ptr */
    struct restart_block restart_block;
};

static inline __attribute_const__ struct thread_info *current_thread_info(void)
{
    register unsigned long sp asm("sp");  //获取sp栈顶指针的地址
    return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); //计算thread_info的地址
    // 注:THREAD_SIZE是内核堆栈的大小,不同体系结构这个值不同
    // 假设这里是8K,那么只要将堆栈的栈顶地址后13位屏蔽掉就可以得到thread_info的地址
    //得到thread_info就可以得到当前进程的task_struct结构体
    //current_thread_info()->task 就是当前进程的task_struct
到此为止,我相信大家对怎么快速获取task_struct的细节应该有所理解了。
全局还有一个current宏,表示的就是当前进程的task_struct地址
#define get_current()   (current_thread_info()->task)
#define current     get_current()

下面是进程内核栈的图示:
Linux内核原理-进程入门_第2张图片

进程的状态

在task_struct中的state字段表示的就是进程的状态

struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */
    unsigned int ptrace;
    .......
在这里使用了volatile来修饰state变量,主要是因为state需要实时从内存访问最新的值,而不是读取寄存器中的这个值默认gcc会对其进
行优化,将state的值读入寄存器,每次访问寄存器中值,但是在多线程的环境中,state的值随时可能会改变,所以加入了volatile来修
饰。从注释可以看出 state有三个值,-1 0 或者大于0,分别表示不可运行,就绪,停止,对于停止又细分出很多种,可以看下内核中对进程状态的定义:
#define TASK_RUNNING        0    //正在运行或可以被执行
#define TASK_INTERRUPTIBLE  1    //可中断的,进程正在睡眠(也就是被阻塞了)等待某些条件达成
#define TASK_UNINTERRUPTIBLE    2 //不可中断,不被信号唤醒
#define __TASK_STOPPED      4 //进程停止运行
#define __TASK_TRACED       8//被其他进程追踪

进程状态转化如下图:
Linux内核原理-进程入门_第3张图片
内核中一些操作进程状态的函数:

#define __set_current_state(state_value)            \
    do { current->state = (state_value); } while (0)
#define set_current_state(state_value)      \
    set_mb(current->state, (state_value))

#define barrier() __asm__ __volatile__("": : :"memory")
#define mb() barrier()
#define set_mb(var, value)  do { var = value; mb(); } while (0)
提供了两个宏用来设置当前进程的状态,_set_curent_state这个应该很好理解
而set_current_state则是带有内存屏障的,常用于多处理和多线程的情况 

注:对于内存屏障可以参考这篇博文,更多关于内存屏障的知识可以谷歌

进程的上下文

所谓的进程上下文其实就是进程的当前状态,一组状态数据而已,在进程切换的时候,保存当前进程的数据,载入要切换进程的数据,这样就可以实现进程的切换。状态数据通常包含CPU所有寄存器的值,进程的状态,堆栈中的内容等等。

需要传入运行队列,切换之前的进程task_struct和切换之后的进程task_struct
static inline void context_switch(struct rq *rq, struct task_struct *prev,struct task_struct *next)
在这个函数中做了一下几件事:
1.开始进行两个进程的地址空间切换
需要注意的是如果是当前是线程,那么active_mm指向其所属进程的地址空间mm为NULL,如果进程则active_mm是NULL
mm指向其地址空间
    mm = next->mm;
    oldmm = prev->active_mm;
    if (!mm) {                  //next是线程。则直接使用prev的地址空间
        next->active_mm = oldmm;    //那么地址空间指向prev的地址空间即可
        atomic_inc(&oldmm->mm_count); //因为内核线程不访问用户态地址空间,所以这里惰性TLB模式
        enter_lazy_tlb(oldmm, next);
    } else   
        switch_mm(oldmm, mm, next);  //next是进程,进行页表的切换
    if (!prev->mm) {    //如果prev是内核线程,那么切换后,设置运行队列的地址空间指向oldmm
        prev->active_mm = NULL;
        rq->prev_mm = oldmm;
    }

2.寄存器和堆栈的切换
switch_to(prev, next, prev);

相关知识点补充:
页表有两种,内核页表和进程页表,每个进程都有自己的页表,放在task_struct.pgd中,内核页表放在init_mm.pgd在保护模式下,从硬件角度看,其运行的基本对象为”进程”(或线程,而寻址则依赖于”进程页表”,在进程调度而进行上下文切换时,会进行页表的切换:即将新进程的pgd(页目录)加载到CR3寄存器中。内核页表中的内容为所有进程共享,每个进程都有自己的”进程页表”,”进程页表”中映射的线性地址包括两部分:用户态,内核态.其中内核态地址对应的相关页表项,对于所有进程来说都是相同的(因为内核空间对所有进程来说都是共享的),而这部分页表内容其实就来源于”内核页表”,即每个进程的”进程页表”中内核态地址相关的页表项都是“内核页表”的一个拷贝。

你可能感兴趣的:(linux内核基础)