linux驱动开发-进程管理

来源:互联网 发布:倩女幽魂能用mac电脑吗 编辑:程序博客网 时间:2024/06/02 04:13

学习实际的驱动编程之前,有必要对内核进程管理等概念有个基本认识,以下是以前学习进程管理的比较。

/****************************

 *  进程管理
 ****************************/

(1)进程的概念
进程是linux最基本的抽象之一,进程是处于执行期的程序以及他所包含的资源的总称。
进程包含的资源有:
*一段可执行的程序代码(代码段)
*存放全局变量的数据段
*打开的文件
*挂起的信号
*内核内部数据
*处理器状态
*地址空间
*一个或多个线程等

进程称为process。进程的另一个名字是任务(task)。linux常常把进程叫做任务。


(2)线程的概念
线程(thread)是在进程中活动的对象。每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器。
注意!linux内核调度的对象是线程,而不是进程。对linux而言,线程只不过是共享资源的特殊进程。


(3)进程描述符及任务结构task_struct
在内核中,通过一个结构体task_struct来描述并管理一个进程,task_struct称为进程描述符(process descriptor),包含了一个具体进程的所有信息。结构定义在<linux/sched.h>中。

内核通过一个称为任务队列的双向循环链表将所有的task_struct组织起来。

task_struct在32位机器上大约有1.7k字节。其中包含的数据能够完整描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还有其他更多信息。

参见include/linux/sched.h中的task_struct


(4)进程描述符的分配
2.6内核以前,task_struct在内核栈中分配。
2.6内核则通过slab分配器分配。

为了方便x86或者arm这种寄存器比较少的处理器可以快速查找到task_struct,2.6在在进程的内核栈中分配一个新的结构thread_info,以方便通过栈指针找到task_struct的位置。

内核提供一个唯一的进程标识符PID来表示每个进程。用隐含类型pid_t声明。实际就是一个int类型。PID的最大默认值为32768,可以修改/proc/sys/kernel/pid_max来提高上限。


(5)通过current宏获得task_struct
在内核中,可以通过宏current获得当前进程的task_struct。只要包含了<linux/sched.h>就可以使用current宏。

current宏定义在<asm/current.h>
thread_info结构体和汇编级操作函数定义在
<asm/thread_info.h>


(6)进程状态
进程描述符的state域描述了进程的当前状态。系统中的每个进程都处于8种状态(2.6.14)中的一种(状态定义在sched.h中):

#define TASK_RUNNING    0
进程是可执行的,他或者正在执行,或者在运行队列中等待执行。除非主动放弃cpu,进程在用户空间始终处于这一状态。

#define TASK_INTERRUPTIBLE    1
进程正在睡眠。一旦达成某些条件,内核就会唤醒这个进程。进程也可能被信号唤醒。

#define TASK_UNINTERRUPTIBLE    2
进程不能被信号唤醒。也就是在用户空间不能通过kill命令将其杀死。

#define TASK_STOPPED    4
进程停止执行,没有投入运行也不能运行。这种状态发生在接收到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOUT等信号时。

#define EXIT_ZOMBIE    16
该进程已经结束,但其父进程还没有调用wait4()系统调用。为了父进程能够获得它的消息,子进程的task_struct仍然保留,直到父进程调用了wait4()。

其他:
#define TASK_TRACED    8
#define TASK_NONINTERACTIVE    64
/* in tsk->exit_state */
#define EXIT_DEAD    32

进程的状态转换(图)

在内核中常常要修改进程的状态
可以用函数
  set_task_state(task, state);
或直接:
  task->state = state;


(7)进程的家族树
进程间存在明显的继承关系。所有的进程都是pid为1的init进程的后代。内核在系统启动的最后阶段启动init进程,该进程读取系统的初始化脚本并执行其他的相关程序。参见/init/main.c。
在rest_init中启动内核线程kernel_thread(init...),这就是init进程。实际的执行代码在init()函数中。

start_kernel -> rest_init -> init -> /sbin/init

通过进程描述符,可以找到每个进程的parent,sibling,children


(8)进程的创建
进程的创建通过fork()和exec()完成。
fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅在于pid,ppid和某些资源。
exec()负责读取可执行文件并将其载人地址空间运行。

在用户空间创建一个进程可以通过调用fork(),vfork(),和clone()实现,而在linux内核中,这几个调用对应的实现函数定义在/arch/x86/kernel/process.c:
sys_fork; sys_clone; sys_vfork

这三个系统调用最终都会调用do_fork()。do_fork()定义在/kernel/fork.c中。


(9)线程在linux中的实现
linux把所有的线程都当作进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。因为每个线程都有自己的task_struct,所以内核调度实际上针对的是线程。

在用户空间创建线程和进程很类似,只不过在调用clone()时要指明需要共享的资源。


(10)内核线程
内核经常要在后台执行一些操作,这些操作可以通过内核线程(kernelthread,独立运行在内核空间的标准进程)完成。
内核线程的特点:
*它们以内核态运行在内核地址空间
*它们不于用户直接交互,因此不需要终端设备
*它们通常在系统启动时创建,一直活跃到系统关闭
/*可以用$>ps axj观察 */

内核线程和普通进程的区别在于它没有独立的地址空间(mm指针被设为NULL),只在内核空间运行,从来不切换到用户空间去。内核线程和普通进程一样,可以被调度,也可以被抢占。

linux会把一些任务交给内核线程完成,如pdflush和ksoftirqd,内核线程只能由其他的内核线程创建。

原型定义:
int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)

flags和clone()的一致,如CLONE_KERNEL由CLONEFS,CLONEFILES和CLONESIGHAND组成。

可见driver/mmc/mmc_queue.c


(11)进程的结束
在用户空间通过调用exit()退出进程(C编译器会自动在main()的返回点后放置调用exit()的代码)。

参见kernel/exit.c中的do_exit

进程退出后,与进程相关的所有资源都被释放,进程不可运行,处于EXIT_ZOMBIE状态。他所占用的所有资源就是内核栈,thread_info结构和task_struct结构。


(12)删除进程描述符
do_exit()仍然保留进程的task_struct,通过在父进程中调用wait4()系统调用将这些信息删除。如果父进程在子进程之前退出,系统会在父进程所在线程组中找一个线程作为父亲,如果不行,则让init进程作为父亲。
do_exit() --> notify_parent() -->forget_original_parent()



/****************************
 *  进程调度
 ****************************/

(1)调度的原理和调度类型
进程调度程序是内核的组成部分,它负责选择下一个要运行的程序。linux通过时钟中断触发调度程序,与调度相关的代码几乎都在kernel/sched.c和include/linux/sched.h中。

多任务系统可分为两类:
非抢占式多任务(cooperative multitasking)
抢占式多任务(preemptive multitasking)

linux采用了抢占式多任务模式。由调度程序决定什么时候停止一个进程的运行以便其他进程能够得到执行机会。强制的挂起动作叫做抢占(preemption)。

进程在被抢占之前能够运行的时间是预先设定好的,叫进程的时间片(timeslice),时间片实际上就是分配给每个可运行进程的处理器时间段。

大多数的操作系统都采用了抢占式多任务,如果采用非抢占式多任务模式,则除非进程自己主动停止运行,否则它会一直执行。进程主动挂起自己的行为称为让步(yielding).


(2)调度策略
调度策略决定调度程序在何时让什么进程运行。进程可以被分为I/O消耗型(如键盘输入)和处理器消耗型(如循环)。调度要在两个矛盾的目标中寻找平衡:进程响应迅速和最大系统利用率。linux为了保证交互式应用,会优先调度I/O消耗型进程。

linux基于优先级调度,提供了两种优先级:
动态优先级
普通进程采用动态优先级,动态优先级分为两部分(静态优先级和调整值),静态优先级用nice值表示,范围从-20到19,默认值为0。nice值越大优先级越低,并且时间片越短。nice值是在进程创建时从父进程继承来的,存放在task_struct的static_prio中。可以通过nice()系统调用修改进程的nice值

系统会根据进程的类型在static_prio的基础上对进程的优先级进行范围-5到5的调整,如I/O消耗型进程的优先级会被动态提高。调整后的值就是动态优先级,存放在task_struct的prio中,调度程序就是根据prio对进程进行调度的。

实时优先级
实时进程采用实时优先级,其值可配,默认的范围从0到99。任何实时进程的优先级都高于普通进程。也就是说只有当实时进程主动放弃cpu时才能调度普通进程。实时优先级存放在task_struct的中rt_priority


(3)进程的时间片
时间片是一个数值,表明进程在被抢占前所能持续运行的时间。时间片太长会使交换性变差,而时间片太短会明显增大进程切换带来的处理器耗时。

当一个进程的时间片耗尽后,就不会再投入运行,一直到系统中所有处于运行态的进程都把时间片耗尽。此时,所有处于运行态的进程会根据静态优先级重新获得时间片并投入运行。

新创建的子进程和父进程平分时间片。当进程的时间片用完后,需要根据其静态优先级static_prio重新计算时间片,通过time_slice()计算。优先级和时间片地对应关系如下:
nice=+19 <--> 时间片5毫秒(MIN_TIMESLICE)
nice=0 <--> 时间片100毫秒(DEF_TIMESLICE)
nice=-20 <--> 时间片800毫秒(MAX_TIMESLICE)


(4)抢占和上下文切换
linux的调度是在kernel/sched.c的schedule()函数中完成的,schedule()调用完成后,系统或者仍然运行当前进程,或者切换到更高优先级的进程。

如果出现上下文切换,也就是从一个可执行进程切换到另一个可执行进程,则schedule()会调用context_switch()函数。该函数主要完成:
*调用<asm/mmu_context.h>中的switch_mm(),切换虚拟内存
*调用<asm/system.h>中的switch_to(),切换处理器状态,包括保存、恢复栈信息和寄存器信息

系统发生调度的地方只可能有三处:
1.在内核代码中主动调用schedule()
2.从中断返回
3.从系统调用或异常返回
后两种情况需要检查每个进程的need_resched标志,如果这个标记被schedule_tick()或wake_up()等设定,则调用schedule()。具体实现参加arch/kernel/entry.s


(5)用户抢占和内核抢占
所谓用户抢占就是当进程在用户空间运行时被别的进程抢占了,而内核抢占是指当进程由于调用系统调用而进入内核时,被别的进程抢占。
2.4只支持用户抢占,而2.6支持完全的内核抢占,只有没有持有锁,就可以进行内核抢占。

用户抢占发生在:
*任何时候

内核抢占发生在:
*进程在内核态执行系统调用,此时发生中断,中断返回后可能发生抢占
*当内核代码再一次具有可抢占性时(在单cpu上,获得spinlock锁时会把内核抢占关闭,而释放spinlock锁则打开内核抢占功能)
*内核中的任务显式调用schedule()
*内核中的任务阻塞(同样导致调用schedule())



(6)实时
linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR,普通的非实时策略是SCHED_NORMAL。

当系统中有实时进程运行,那么就永远不会调度非实时进程,除非实时进程自己调用schedule()。SCHED_FIFO为先入先出调度,没有时间片,而SCHED_RR带有时间片。

实时进程的优先级从0到MAX_RT_PRIO-1,默认情况下从0到99,而SCHED_NORMAL进程的优先级从MAX_RT_PRIO到(MAX_RT_PRIO+40),默认为100到139

2.6内核可以满足相当严格的实时要求。

0 0
原创粉丝点击