Linux内核-进程

来源:互联网 发布:美加净护手霜 知乎 编辑:程序博客网 时间:2024/04/28 14:05

       进程的目的就是担当分配资源的实体,一个进程又几个用户线程程。每个线程代表一个执行流。线程的实现方法有三种:1、用户级线程;2、内核级线程;3、混合线程模型。Linux内核中的进程和线程都用相同的数据结构task_struct表示;线程是特殊的进程,共享同一地址空间、共同合作。

       进程描述符都是task_struct,包含了与进程相关的所有信息。一般来说能被调度的每个执行上下文都必须拥有自己的task_struct,即使是轻量级线程。

 

进程状态

       可执行状态(TASK_RUNNING):处于这个状态要么在CPU上执行,要么准备执行。

       可中断的睡眠状态(TASK_INTERRUPTIBLE):进程被挂起,指导某个条件变为真,产生一个硬件中断,释放进程正在等待的资源或传递一个信号把进进程唤醒,从而使等待的进程回到TASK_RUNNING。

       不可中断的睡眠状态(TASK_UNINTERRUPTIBLE):不能通过信号唤醒。

       暂停状态(TASK_STOPPED ):进程收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号进入暂停状态。

       跟踪状态( TASK_TRACED):进程的执行已由debugger程序暂停。

       僵死状态(EXIT_ZOMBIE):进程的执行被终止,但是父进程还没有发布wait4()或waitpid()系统调用来返回关于死亡进程的信息。发布wait()系统调用之前,内核不能丢弃包含在死亡进程描述符中的数据,因为父进程还需要它。

       僵死撤销状态(EXIT_DEAD):父进程刚发布wait4()或waitpid()系统调用,因而进程被删除。

 

task_struct结构

Linux用task_struct结构表示进程,2.6内核的task_struct结构相对于2.4内核有很大变化。该结构记录了进程的重要信息,与进程调度有关的信息包括:
(1)state
        进程状态由state成员变量表示。一个进程共有7种可能状态,分别是:TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE、TASK_STOPPED、TASK_TRACED、EXIT_ZOMBIE和EXIT_DEAD
(2)timestamp
        进程发生调度事件的时间(单位是 nanosecond)。
(3)prio, static_prio
        进程的优先级和静态优先级。Prio表示进程的动态优先级,与2.4内核中goodness()的计算结果相当。在0~MAX_PRIO-1之间取值(MAX_PRIO 定义为 140),其中0~MAX_RT_PRIO-1(MAX_RT_PRIO定义为100)属于实时进程范围,MAX_RT_PRIO~MX_PRIO-1属于非实时进程。数值越大,表示进程优先级越小。static_prio表示进程的静态优先级,相当于2.4内核中的nice。一个进程的初始时间片的大小完全取决于它的静态优先级。
(4)sleep_avg
        进程的平均等待时间,在0到NS_MAX_SLEEP_AVG之间取值,初值为0,相当于进程等待时间与运行时间的差值。它是动态优先级计算的关键因子,sleep_avg越大,计算出来的进程优先级也越高。
(5)interactive_credit
         该变量表示进程的交互程度。在-CREDIT_LIMIT到CREDIT_LIMIT+1之间取值,初始值为0,而后根据不同的条件加1减1,一旦该值超过CREDIT_LIMIT,即表示该进程是交互进程。
(6)array
        指向当前CPU的active就绪进程队列。
(7)run_list
        进程通过这个list_head变量连接自己到prio_array数组中queue队列,这样相同优先级的进程连接成一个双向列表,表头为prio_array结构中的queue变量。


进程描述符

       Unix系统通过pid来标识进程,linux把不同的pid与系统中每个进程或轻量级线程关联,而unix程序员希望同一组线程具有共同的pid,遵照这个标准linux引入线程组的概念。一个线程组所有线程与领头线程具有相同的pid,存入tgid字段,getpid()返回当前进程的tgid值而不是pid的值。

       Linux把thread_info(线程描述符)和内核态的线程堆栈存放在一起,这块区域通常是8192K(占两个页框),其实地址必须是8192的整数倍。从图中看出thread_info和task_struct结构互联。


union thread_union {

       struct thread_info thread_info;

       unsigned long      stack[2048];

}

       esp是寄存器CPU栈指针。用来存放栈顶单元的地址。栈是从高地址向下生长,用户态刚切换到内核态以后,进程的内核栈总是空的,esp指向顶端。一旦有数据写入,esp就递减。thread_info是52字节大小,所以内核栈最大能扩大8140个字节。

      可以很容易从esp寄存器获得当前正在CPU上运行的进程thread_info结构的地址。只要取高13位,低8位置0就行了。其实宏current_thread_info()实现的功能,因为thiread_info->task偏移地址为0,所以该宏等价于current_thread_info()->task。

movl $0xffffe000  %ecx

andl%esp  %ecx

movl %ecxp  //地址存在p


注:有关内核栈和用户栈的内容参考本博客:linux内核-进程内核栈、用户栈


进程运行队列(就绪队列)

        Linux内核定义了一个list_head数据结构(有关listh_head结构参考本博客list_head结构),字段head和prev分别表示通用的双向链表向前和向后的指针元素。每个task_struct包含一个list_head类型的run_list字段。


       在Linux 2.4内核中,就绪进程队列是一个全局数据结构,所有的处理器共享同一个队列。调度器对它的所有操作都会因全局自旋锁而导致系统各个处理机之间的等待,使得就绪队列成为一个明显的瓶颈[2,3]。2.6内核重新设计就绪进程队列为每CPU的数据结构,每个处理器都维护一个自己的就绪队列,这样就避免了2.4内核中的SMP性能瓶颈。

       每个CPU的就绪进程队列由一个struct  runqueue结构描述,其中最关键的子结构是优先级就绪数组。每个runqueue包含两个优先级就绪数组:active和expired数组。active 指向时间片没用完、当前可被调度的就绪进程,expired 指向时间片已用完的就绪进程。当一个进程的时间片耗尽后,内核在将其放入expired数组前单独计算该进程的时间片。而当active数组为空时(即active数组中所有进程的时间片全部耗尽),通过简单的调换active和expired指针实现所有进程时间片的重算,该过程是O(1)量级的,与系统中的进程数目无关。

描述优先级就绪数组的数据结构是prio_array_t,定义为:

struct prio_array {
        int               nr_active;//链表中进程描述符的数量         
        unsigned long     bitmap[BITMAP_SIZE];  //优先权位图,优先权队列不为空则置位
        struct list_head  queue[MAX_PRIO]; //140个优先权队列的头结点
};

        Runqueue结构中另两个重要的成员变量是best_expired_prio和expired_timestamp。前者记录expired 就绪进程组中的最高优先级,后者用来表征 expired 中就绪进程的最长等待时间。

       进程调度由schedule()函数实现。首先,schedule()利用下面的代码定位优先级最高的就绪进程。schedule()通过调用sched_find_first_bit()函数找到当前CPU就绪进程队列runqueue的active进程数组中第一个非空的就绪进程链表。这个链表中的进程具有最高的优先级,schedule()选择链表中的第一个进程作为调度器下一时刻将要运行的进程。如果prev(当前进程)和next(将要运行的进程)不是同一个进程,schedule()调用context_switch()将CPU切换到next进程运行。

prev current;
array rq->active;//array为active数组
idx sched_find_first_bit(array->bitmap);//首先查找位图
queue array->queue idx;//指向相应的优先权队列
next list_entry(queue->next, struct task_struct, run_list);//优先权队列的第一个元素作为next 

 

进程等待队列

        Linux等待队列在内核中有很多用途,尤其在中断处理、进程同步以及定时。例如等待一个磁盘操作的终止,等待释放资源,或等待时间经过的固定的时间间隔。等待队列实现了事件上的条件结构:希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此等待队列表示一组睡眠的进程,当某一条件为真时,由内核唤醒它们。

等待队列由双向链表实现。

 

struct __wait_queue_head {  

        spinlock_t lock;  

        struct list_head task_list;  

};  //等待队列的头结点,不包含其他信息

typedef struct__wait_queue_head wait_queue_head_t;  

struct __wait_queue {  

       unsigned int flags; //标识是互斥进程还是非互斥进程 

       #defineWQ_FLAG_EXCLUSIVE      0x01  /* 表示等待进程想要被独占地唤醒  */  

       void *private;               /* 指向等待进程的task_struct实例 */  

       wait_queue_func_t func;      /* 用于唤醒等待进程              */  

       sruct list_head task_list;  /* 用于链表元素,将wait_queue_t链接到wait_queue_head_t */  

}; 

typedef struct__wait_queue wait_queue_t;  

有关等度列的一些操作:

       init_waitqueue_entry():动态初始化

       DEFINE_WAIT():静态初始化

       add_wait_queue():把进程加入等度队列第一个位置

       add_wait_queue_exclusive():把进程加入等度队列最后一个位置

       remove_wait_queue():移除队列

       sleep_on():把当前进程设为task_uniterruptible,并插入等待队列,然后调用shedule()函数

       sleep_on_timeout():睡眠进程的时候可以设一个时间间隔,过了间隔以后,由内核唤醒

       prepare_wait():提供了另一种途径睡眠进程

       wake_up()、wake_up_nr()、wake_up_all():唤醒进程

 

进程切换

       内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程执行。这种行为称为进程切换、任务切换或上下文切换。尽管每个进程都拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装了挂起进程时的值。进程恢复前必须装入寄存器的一组数据称为硬件上下文,它的一部分存在TSS段,而剩余部分存放在内核态堆栈中。

       在每次进程切换时,被替换的硬件上下文必须保存在别处,不能像Intel原始设计那样把它保存在TSS中,因为Linux为每个器而不是每个进程使用TSS。因此每个进程描述符包含一个类型thread_structthread字段,来保存进程切换时的硬件上下文。

       进程切换发生在schedule()函数,包括两步:(1)切换全局目录以及安装一个新的地址空间;(2)切换内核态堆栈和硬件上下文。(这一步由宏执行swith_to,又调用__swith_to()函数)

原创粉丝点击