Linux内核原理-进程入门

来源:互联网 发布:外文电子图书数据库有 编辑:程序博客网 时间:2024/04/27 03:12

进程

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

进程描述符及任务结构

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

进程之间的关联如下图:
这里写图片描述

进程描述符的分配

在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()

下面是进程内核栈的图示:
这里写图片描述

进程的状态

在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//被其他进程追踪

进程状态转化如下图:
这里写图片描述
内核中一些操作进程状态的函数:

#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_structstatic inline void context_switch(struct rq *rq, struct task_struct *prev,struct task_struct *next)在这个函数中做了一下几件事:1.开始进行两个进程的地址空间切换需要注意的是如果是当前是线程,那么active_mm指向其所属进程的地址空间mm为NULL,如果进程则active_mm是NULLmm指向其地址空间    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寄存器中。内核页表中的内容为所有进程共享,每个进程都有自己的”进程页表”,”进程页表”中映射的线性地址包括两部分:用户态,内核态.其中内核态地址对应的相关页表项,对于所有进程来说都是相同的(因为内核空间对所有进程来说都是共享的),而这部分页表内容其实就来源于”内核页表”,即每个进程的”进程页表”中内核态地址相关的页表项都是“内核页表”的一个拷贝。

0 0
原创粉丝点击