进程上下文 down up 上锁
添加节点
删除节点
关键代码解释
链表是内核开发中的常见数据组织形式,因此为了方便开发和统一结构,内核提供了一套辅助例程帮助操作链表:声明链表结构LIST_HEAD()、
加节点到链表list_add()、删除节点list_del()、遍历链表list_entry()。
任务队列结构 struct tq_struct
STEP BY STEP——观察结果
结果中可看到,内核线程加入节点速度很快,每次时钟间隔(删除节点)期间都可加入一百个左右的节点(由于内核中还有其他任务运行,影响内核线程和定时器任务运行频率,所以定时器任务执行期间加入节点数会有所不同)。 由于内核线程填加节点速度快于定时器任务删除
节点速度,所以很快就链表长度就会达到我们指定长度(800),这时新内核线程就会从删除头节点(最早加入的节点)。 结束
并发的发生随处都有,但是有它引起的错误可并非每次都有,因为并发过程中引起错误的地方往往就一两步,因此交错执行这一两步要靠“运气”,出错的几率有时很小。但是一旦发生后果都是灾难性的,比如宕机,破坏数据完整性等。所以我们对并发绝不能掉以轻信,必须拿出“把纸老虎当真老虎的”决心来对待一切内核代码中的并发可能,即便在单处理器上编程也需要到移植到多处理器的情况,总之一切都要谨慎小心。
进程的管理与调度
进程管理
进程描述符及任务结构
进程存放在叫做任务队列(tasklist)的双向循环链表中。链表中的每一项包含一个具体进程的所有信息,类型为task_struct,称为进程描述符(process descriptor),该结构定义在
Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。另一方面,为了避免使用额外的寄存器存储专门记录,让像x86这样寄存器较少的硬件体系结构只要通过栈指针就能计算出task_struct的位置,该结构为thread_info,在文件中定义。 Linux中可以用ps命令查看所有进程的信息。
Linux基础篇之内存管理机制 http://www.linuxidc.com/Linux/2014-03/98293.htm
Linux内存管理之高端内存 http://www.linuxidc.com/Linux/2013-06/85693.htmLinux内存管理之分段机制 http://www.linuxidc.com/Linux/2012-11/74480.htmLinux内存管理伙伴算法 http://www.linuxidc.com/Linux/2012-09/70711.htm
进程状态
task_struct中的state描述进程的当前状态。进程的状态一共有5种,而进程必然处于其中一种状态:
1)TASK_RUNNING(运行)——进程是可执行的,它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间中执行唯一可能的状态;也可以应用到内核空间中正在执行的进程。
2)TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行,处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。
3)TASK_UNINTERRUPTIBLE(不可中断)——除了不会因为接收到信号而被唤醒从而投入运行外,这个状态与可打断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不作响应,所以较之可中断状态,使用得较少。
4)TASK_ZOMBIE(僵死)——该进程已经结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait4(),进程描述符就会被释放。
5)TASK_STOPPED(停止)——进程停止执行,进程没有投入运行也不能投入运行。通常这种状态发生在接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
需要调整进程的状态,最好使用set_task_state(task, state)函数,在必要的时候,它会设置内存屏障来强制其他处理器作重新排序(SMP)。 进程的各个状态之间的转化构成了进程的整个生命周期,
进程的创建
在Linux系统中,所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个进程。
Linux提供两个函数去处理进程的创建和执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一),PPID(父进程的PID)和某些资源和统计量(例如挂起的信号)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
fork()使用写时拷贝(copy-on-write)页实现。内核在fork进程时不复制整个进程地址空间,让父进程和子进程共享同一个拷贝,当需要写入时,数据才会被复制,使各进程拥有自己的拷贝。在页根本不会被写入的情况下(fork()后立即exec()),fork的实际开销只有复制父进程的页表以及给子进程创建唯一的task_struct。
创建进程的fork()函数实际上最终是调用clone()函数。创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0);创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGHAND, 0)。
在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。 fork和vfork的区别
fork()与vfock()都是创建一个进程,那他们有什么区别呢?总结有以下三点区别: 1. fork ():子进程拷贝父进程的数据段,代码段 vfork ( ):子进程与父进程共享数据段 2. fork ()父子进程的执行次序不确定
vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec 或exit 之后父进程才可能被调度运行。
3. vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在
调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
Linux内核之CFS调度和组调度
Linux支持三种进程调度策略,分别是SCHED_FIFO 、 SCHED_RR和SCHED_NORMAL。Linux支持两种类型的进程,实时进程和普通进程。实时进程可以采用SCHED_FIFO 和SCHED_RR调度策略;普通进程采用SCHED_NORMAL调度策略。
本文主要讨论普通进程的调度算法,为了描述方便,后面章节中的“进程”指“普通进程”。
从Linux2.6.23内核到目前最新的Linux3.3.5内核的普通进程(采用调度策略SCHED_NORMAL)采用了绝对公平调度算法,CFS(completely fair schedule)。CFS从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再区分交互式进程。它将所有的进程都统一对待,这就是公平的含义。CFS 调度中,进程数据结构中的动态优先级成员prio还继续有效,只是内核不再动态调整进程的动态优先级了。
进程的优先级为100—139,对应的nice值为-20—19。和之前版本的优先级规定相同。Nice 和优先级对应关系如下
如何实现公平调度的?内核给每个进程维护了一个虚拟运行时间vruntime,每个进程运行一段时间后,虚拟运行时间会增加,但是运行同样的实际时间每个