Linux进程分析

来源:互联网 发布:java io应用实例 编辑:程序博客网 时间:2024/05/22 07:08

多任务系统可以分为:非抢占式和抢占式。Linux提供抢占式多任务模式。进程在被抢占之前能够运行的时间叫进程的时间片,Linux独一无二的公平调度程序本身并没有采用时间片来达到公平调度。

  Linux之前采用O(1)调度器,它对大服务器的工作负载很理想,但是对响应时间敏感的程序却有不足。在2.6.23内核版本中用完全公平调度算法(CFS)代替了O(1)调度算法。

  进程可以被分为I/O消耗型和处理器消耗型。I/O消耗型指进程的大部分时间用来提交I/O请求或是等待I/O请求。处理器消耗型指进程把事件大多数用在执行代码上。调度策略通常在两个矛盾中寻找平衡:进程响应迅速和最大系统利用率。Linux更倾向于优先调度I/O消耗型进程。

  调度程序总是选择时间片未用尽而且优先级最高的进程运行。

  Linux采用两种不同的优先级范围。第一种是nice值,越大的nice值意味着更低的优先级。第二种是实时优先级,其值可以配置,越高的实时优先级数值意味着进程优先级越高。任何实时进程的优先级都高于普通进程。

  CFS做法:允许每个进程运行一段时间,循环轮转,选择运行最少的进程作为下一个运行进程,而不再采用分配给每一个进程时间片的做法,CFS在所有可运行进程总数基础上计算出一个进程应该运行多久,而不是依靠nice值来计算时间片。CFS引入每个进程获得的时间片底线(最小粒度)默认情况为1ms,绝对的nice值不再影响调度决策,任何进程所获得的处理器时间由它自己和其他所有可运行进程nice值的相对差决定。

  Linux调度的实现:

  时间记账

  调度器实体结构是struct sched_entity,它嵌入在进程描述符struct task_struct中。虚拟时间以ns为单位,存在vruntime变量中,它和定时器节拍不再相关,vruntime变量可以准确测定进程的运行时间,而且可以知道谁应该是下一个被运行的进程。

  进程选择

  当CFS需要选择下一个运行进程时,它会挑一个具有最小vruntime的进程,这就是CFS调度算法的核心:选择具有最小的vruntime的任务。这里采用红黑树实现的。

  调度器入口

  进程调度的主要入口点是schedule函数,该函数会以优先级为序,从高到低,依次检查每一个调度器,并且从最高优先级的调度器中,选择最高优先级的进程。

  睡眠和唤醒

  对于睡眠,内核的操作是:进程把自己标记为休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule函数选择和执行一个其他进程。对于唤醒,正好相反,进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。

  休眠有两种进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE。它们唯一的区别在于处在TASK_UNINTERRUPTIBLE的进程会忽略信号,处于TASK_INTERRUPTIBLE状态的进程如果接收到一个信号,会被提前唤醒并响应该信号。两种状态的进程位于同一个等待队列上,等待某些事情,不能够运行。

  关于休眠还需要注意一点:存在虚假的唤醒,所以有时候需要一个循环处理来保证进程被唤醒的条件真正大达成。

  等待队列的使用

  static DECLARE_WAIT_QUEUE_HEAD(button_waitq);  //定义等待队列头
  wait_event_interruptible(button_waitq, ev_press);              //休眠
  wake_up_interruptible(&button_waitq);                  //唤醒

  抢占和上下文切换

  上下文切换就是从一个可执行进程切换到另一个可执行进程。Schedule函数主要完成两个工作:其一,把虚拟内存从上一个进程映射切换到新进程中。其二,从上一个进程的处理器状态切换到新进程的处理器状态。

  内核必须知道在什么时候调用schedule函数,如何仅靠用户程序代码显式地调用schedule,它们可能就会永远的执行下去,所以内核中提供了一个need_resched标志来表明是否需要重新执行一次调度,该标志对于内核来讲是一个信息,它表示有其他进程应当被运行了,要尽快调用调度程序。

  用户抢占

  内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule函数被调用,此时就会发生用户抢占。用户抢占在如下情况时产生:其一,从系统调用返回用户空间时。其二,从中断处理程序返回用户空间时。

  内核抢占

  在2.6版内核中,内核引入抢占能力,只要重新调度是安全的,即只要没有持有锁,内核就可以在任何时候抢占正在执行的任务。为了支持内核抢占,在每个进程的thread_info引入preempt_count计数器,当做一把锁来使用。内核抢占在如下情况时产生:其一,中断处理程序正在执行,且返回内核空间之前。其二,内核代码再一次具有可抢占性的时候。其三,如果内核中的任务显式地调用schedule函数。其四,如果内核中的任务阻塞(这样会调用schedule函数)。


进程是正在执行的程序代码的实时结果,是处于执行期的程序以及相关的资源的总称。线程是在进程中活动的对象,内核调度的对象是线程,而不是进程。对Linux系统而言,不区分线程和进程。进程提供两种虚拟机制:虚拟处理器和虚拟内存。线程之间可以共享内存,但每个都拥有各自的虚拟处理器。在Linux中,通过fork创建子进程,子进程通过exit()系统调用终结进程并将其占用的资源释放掉,此时子进程被设置为僵死状态,父进程可以通过wait()waitpid()系统调用查询子进程是否终结。

 

内核把进程的列表存放在叫任务队列(task_list)的双向循环列表中,链表中的每一个项都是类型为task_struct,称为进程描述符。Linux通过slab分配器分配task_struct结构,由于现在用的slab分配器动态生成task_struct,所以只需要在栈低或者栈顶创建一个新的结构体struct thread_info。内核通过进程标识值PID来标识每个进程,内核把每个进程的PID存放在它们各自的进程描述符中,所有的进程都是PID1init进程的后代,PID最大默认值为32768,这个值越小转一周就越快,系统管理员可以通过修改/proc/sys/kennel/pid_max来提高上限。

 

进程五种状态标志:TASK_RUNNINGTASK_INTERRUPTIBLETASK_UN INTERRUPTIBLE_TASK_TRACED_TASK_STOPPED

 

进程上下文

一般程序在用户空间执行,当一个程序执行了一个系统调用或者触发了某个异常,它就陷入内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中。除非在此间隙有更高优先级的进程需要执行并且由调度器做出了相应的调整,否则在内核退出的时候,程序恢复在用户空间会继续执行。系统调用和异常处理程序是对内核明确定义的接口,对内核的所有访问都必须通过这些接口。

 

进程创建

子进程和父进程区别仅仅在于PID(每个进程唯一)PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(如挂起的信号)Linuxfork()使用写时拷贝页实现,写时拷贝页是一种可以推迟甚至免除拷贝数据的技术,内核此时并不是赋值整个进程地址空间,而是让父进程和子进程共享同一个拷贝。fork()的实际开销就是赋值父进程的页表以及给子进程创建唯一的进程描述符。内核有意会选择子进程先执行,因为一般子进程都会马上调用exec()函数,这样避免了写时拷贝页的额外开销,如果父进程先执行的话,有肯能会开始向地址空间写入。Vfork()除了不拷贝父进程的页表项外,其他跟fork一样,由于vfork语意微妙,系统最好不要用这个函数。

 

线程创建

用户空间创建线程可以用clone函数创建,但我们的重点是内核线程,内核常常需要在后台执行一些操作,这种任务可以通过内核线程完成。内核线程和普通进程的区别在于内核线程没有独立的地址空间,它们只在内核空间运行,从来不切换到用户空间去,内核线程和普通进程一样,可以被调度,可以用被抢占。

内核通过从kthreadd内核进程中衍生出所有新的内核线程的,于是,从现有内核线程中创建一个新的内核线程有两种方法。其一,利用kthread_creat()函数创建,并用wake_up_process()唤醒。其二,直接执行kthread_run()。内核线程启动之后就一直运行直到调用do_exit()退出,或者内核的其他不烦你调用kthread_stop()退出。

 

 

在调用exit()结束子进程后,尽管线程已经僵死不能运行了,但系统还保留了它的进程描述符,这样做可以让系统有办法在子进程终结后仍能获得它的信息。所以,进程终结时需要将清理工作和删除进程描述符工作分开执行,我们的exit就完成的清理工作,删除进程描述符由父进程中的wait函数完成。Wait函数的标准动作是挂起调用它的进程,直到其中的一个子进程退出。

 

孤儿进程

如果父进程在子进程之前退出,必须有一个机制来保证子进程能找到一个新的父亲,否则这些孤儿进程就会在退出时永远处于僵死状态,白白耗费内存。解决这个问题的办法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。

转自 http://my.csdn.net/weiqing1981127

0 0
原创粉丝点击