📄 进程.txt
字号:
struct task_struct {
………….
volatile long state; /* -1 unrunnable , 0 runnable , >0 stopped */
unsigned long flags; /* per process flags, defined below */
………….
};
每个在Task向量表中登记的进程都有相应的进程状态和进程标志,是进行进程调度的重要依据。进程在执行了相应的进程调度操作后,会由于某些原因改变自身的状态和标志,也就是改变state和flags这两个数据项。进程的状态不同、标志位不同对应了进程可以执行不同操作。在Linux2.2.8版本的sched.h中定义了六种状态,十三种标志。
//进程状态
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_ZOMBIE 4
#define TASK_STOPPED 8
#define TASK_SWAPPING 16
它们的含义分别是:
TASK_RUNNING:正在运行的进程(是系统的当前进程)或准备运行的进程(在Running队列中,等待被安排到系统的CPU)。处于该状态的进程实际参与了进程调度。
TASK_INTERRUPTIBLE:处于等待队列中的进程,待资源有效时唤醒,也可由其它进程被信号中断、唤醒后进入就绪状态。
TASK_UNINTERRUPTIBLE:处于等待队列中的进程,直接等待硬件条件,待资源有效时唤醒,不可由其它进程通过信号中断、唤醒。
TASK_ZOMBIE:终止的进程,是进程结束运行前的一个过度状态(僵死状态)。虽然此时已经释放了内存、文件等资源,但是在Task向量表中仍有一个task_struct数据结构项。它不进行任何调度或状态转换,等待父进程将它彻底释放。
TASK_STOPPED:进程被暂停,通过其它进程的信号才能唤醒。正在调试的进程可以在该停止状态。
TASK_SWAPPING:进程页面被兑换出内存的进程。这个状态基本上没有用到,只有在sched.c的count_active_tasks()函数中判断处于该种状态的进程也属于active的进程,但没有对该状态的赋值。
//进程标志位:
#define PF_ALIGNWARN 0x00000001
#define PF_STARTING 0x00000002
#define PF_EXITING 0x00000004
#define PF_PTRACED 0x00000010
#define PF_TRACESYS 0x00000020
#define PF_FORKNOEXEC 0x00000040
#define PF_SUPERPRIV 0x00000100
#define PF_DUMPCORE 0x00000200
#define PF_SIGNALED 0x00000400
#define PF_MEMALLOC 0x00000800
#define PF_VFORK 0x00001000
#define PF_USEDFPU 0x00100000
#define PF_DTRACE 0x00200000
其中PF_STARTING没有用到。
PF_MEMEALLOC和PF_VFORK这两个标志位是新版本中才有的。
各个标志位的代表着不同含义,对应着不同调用:
PF_ALIGNWARN 标志打印“对齐”警告信息,只有在486机器上实现
PF_STARTING 进程正被创建
PF_EXITING 标志进程开始关闭。
在do_exit()时置位。
current->flags |= PF_EXITING
用于判断是否有效进程。
在nlmclnt_proc()(在fs\lockd\clntproc.c),如果current_flag为PF_EXITING,则进程由于正在退出清除所有的锁,将执行异步RPC
调用。
PF_PTRACED 进程被跟踪标志,
在do_fork()时清位。
p->flags &= ~PF_PTRACED
当ptrace(0)被调用时置位,在进程释放前要清掉。
current->flags |= PF_PTRACED
在sys_trace()中判断
如果request为PTRACE_TRACEME,如是则将current_flag置为PF_PTRACED;
如果request为PTRACE_ATTACH,则将child_flag置为PF_PTRACED,给child发一个SIGSTOP信号;
如果request为PTRACE_DETACH ,则将child清除PF_PTRACED。
在syscall_trace()中判断current_flag如果为PF_TRACED和PF_TRACESYS,则current强行退出时的出错代码置为SIGTRAP并将状态置为STOPPED。
PF_TRACESYS 正在跟踪系统调用。
do_fork()时清位,在进程释放前要清掉。
在sys_trace()中判断request如果为PTRACE_SYSCALL,则将child->flags 置为
PF_TRACESYS;如为PTRACE_SYSCALL,则将child->flags 清除
PF_TRACESYS;然后唤醒child。如果request为PTRACE_SINGLESTEP(即单步跟踪),则将child_flag清除PF_TRACESYS,唤醒child。
PF_FORKNOEXEC 进程刚创建,但还没执行。
在do_fork()时置位。
p->flags |= PF_FORKNOEXEC
在调入格式文件时清位。
p->flags &= ~ PF_FORKNOEXEC
PF_SUPERPRIV 超级用户特权标志。
如果是超级用户进程则置位,用户特权设为超级用户,如是超级用户,在统计时置统计标志(accounting flag)为ASU。
PF_DUMPCORE 标志进程是否清空core文件。
Core文件由gdb进行管理,给用户提供有用信息,例如查看浮点寄存器的内容比较困难,事实上我们可以从内核文件里的用户结构中得到
Core文件格式如下图:
UPAGE
DATA
STACK
Core 文件结构
UPAGE是包含用户结构的一个页面,告诉gdb文件中现有内容所有寄存器也在
UPAGE中,通常只有一页。DATA存放数据区。STACK堆栈区
最小Core文件长度为三页(12288字节)
在task_struct中定义一个dumpable变量,当dumpable==1时表示进程可以清空core文件(即将core文件放入回收站),等于0时表示该进程不能清空core文件(即core文件以放在回收站中,不可再放到回收站中),此变量初值为1。
例如在调用do_aout_core_dump()时判断current->dumpable是否等于1(即判断该进程是否能将core文件放入回收站),如果等于1则将该变量置为0,在当前目录下建立一个core
dump image ,在清空用户结构前,由gdb算出数据段和堆栈段的位置和使用的虚地址,用户数据区和堆栈区在清空前将相应内容写入core
dump,将PF_DUMPCORE置位,清空数据区和堆栈区。
只有在aout_core_dump()内调用do_aout_core_dump(),而没有地方调用aout_core_dump()。对其它文件格式也是类似。
9、 PF_SIGNALED 标志进程被信号杀出。
在do_signal()中判断信号,如果current收到信号为SIGHUP, SIGINT, SIGIOT, SIGKILL, SIGPIPE,
SIGTERM, SIGALRM, SIGSTKFLT, SIGURG, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGPROF,
SIGIO, SIGPOLL, SIGLOST,
SIGPWR,则执行lock_kernel(),将信号加入current的信号队列,将current->flag置为PF_SIGNALED,然后执行do_exit()
PF_USEDFPU 标志该进程使用FPU,此标志只在SMP时使用。
在task_struct中有一变量used_math,进程是否使用FPU。
在CPU从prev切换到next时,如果prev使用FPU则prev的flag清除PF_USEDFPU。
prev->flags&=~PF_USEDFPU
在flush_thread()(arch\i386\kernel\process.c)、restore_i387_hard()、save_i387_hard()(arch\i386\kernel\signal.c)中,如果是SMP方式,且使用FPU则stts(),否则清除PF_USEDFPU。
current->flags &= ~PF_USEDFPU
在sys_trace()中如果request为PTRACE_SETFPREGS,则将child的used_math置为1,将child_flag清除PF_USEDFPU。
child->flags &= ~PF_USEDFPU
在SMP方式下进行跟踪时,判断是否使用FPU。
在跟踪时出现数学错误时清位。
current->flags &= ~PF_USEDFPU
PF_DTRACE 进程延期跟踪标志,只在m68k下使用。
跟踪一个trapping指令时置位。
current->flags |= PF_DTRACE
PF_ONSIGSTK 标志进程是否工作在信号栈,只在m68k方式下使用。
liunx 2.1.19版本中使用此标志位,而2.2.8版本中不使用。
在处理信号建立frame时如果sigaction标志为ONSTACK,则将current->flag置为PF_ONSIGSTK。
PF_MEMALLOC 进程分配内存标志。
linux 2.2.8版本中使用此标志位。
在kpiod()和kwpad()中置位。
tsk->flags |= PF_MEMALLOC
PF_VFORK linux 2.2.8版本中使用此标志位。
在copy_flags(unsigned long clone_flags, struct task_struct
*p),如果clone_flags为CLONE_VFORK,则将p的flags置为PF_VFORK。
在mm_release()中将current ->flags清除PF_VFORK。
tsk->flags &= ~PF_VFORK
具体的分析由我组的另外同学进行。
Linux的各进程之间的状态转换的系统调用
我将参与Linux的各进程之间的状态转换的系统调用总结成一张流程图:
进程的创建:TASK_RUNNING
第一个进程在系统启动时创建,当系统启动的时候它运行在核心态,这时,只有一个进程:初始化进程。象所有其他进程一样,初始进程有一组用堆栈、寄存器等等表示的机器状态。当系统中的其他进程创建和运行的时候这些信息存在初始进程的task_struct数据结构中。在系统初始化结束的时候,初始进程启动一个核心进程(叫做init)然后执行空闲循环,什么也不做。当没有什么可以做的时候,调度程序会运行这个空闲的进程。这个空闲进程的task_struct是唯一一个不是动态分配而是在核心连接的时候静态定义的,为了不至于混淆,叫做init_task。
系统调用sys_fork 和sys_clone都调用函数do_fork()(在kernel/fork.中定义)。
进程由do_fork()函数创建,先申请空间,申请核心堆栈;然后在Task向量表中找到空闲位置;在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLE,以免初始化过程被打断;开始初始化工作,如初始化进程时钟、信号、时间等数据;继承父进程的资源,如文件、信号量、内存等;完成进程初始化后,由父进程调用wake_up_process()函数将其唤醒,状态变为TASK_RUNNING,挂到就绪队列run
queue,返回子进程的pid。
// C:\SRCLNX\KERNEL\FORK.C
int do_fork(unsigned long clone_flags, unsigned long usp, struct pt_regs *regs)
{
为新进程申请PCB空间;
if (申请不到)
返回错误,退出;
为新进程申请核心堆栈;
if (核心堆栈申请不到)
返回错误,退出;
为新进程在Task向量表中找到空闲位置;
/*复制父进程current PCB中的信息,继承current的资源*/;
p = current;
在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLE,以免初始化过程被打断,并置一些标志位.
/*为防止信号、定时中断误唤醒未创建完毕的进 程,将子进程的状态设成不可中断的*/
p->state = TASK_UNINTERRUPTIBLE;
/*跟踪状态和超级用户特权是没有继承性的,因为在root用户为普通用户创建进程时,出于安全考虑这个普通用户的进程不允许拥有超级用户特权。*/
p->flags &= ~(PF_PTRACED|PF_TRACESYS|PF_SUPERPRIV);
/*将进程标志设成初建,在进程第一次获得CPU时,内核将根据此标志进行一定操作*/
p->flags |= PF_FORKNOEXEC;
开始Task_struct的初始化工作,如初始化进程时钟、信号、时间等数据;
继承父进程所有资源:
拷贝父进程当前打开的文件;
拷贝父进程在VFS的位置;
拷贝父进程的信号量;
拷贝父进程运行的内存;
拷贝父进程的线程;
初始化工作结束,父进程将其将其唤醒,挂入running队列中,返回子进程的pid;
}
进程的调度(schedule()):
处于TASK_RUNNING状态的进程移到run queue,会由schedule()按CPU调度算法在合适的时候选中,分配给CPU。
新创建的进程都是处于TASK_RUNNING状态,而且被挂到run queue的队首。进程调度采用变形的轮转法(round
robin)。当时间片到时(10ms的整数倍),由时钟中断引起新一轮调度,把当前进程挂到run queue队尾。
所有的进程部分运行与用户态,部分运行于系统态。底层的硬件如何支持这些状态各不相同但是通常有一个安全机制从用户态转入系统态并转回来。用户态比系统态的权限低了很多。每一次进程执行一个系统调用,它都从用户态切换到系统态并继续执行。这时让核心执行这个进程。Linux中,进程不是互相争夺成为当前运行的进程,它们无法停止正在运行的其它进程然后执行自身。每一个进程在它必须等待一些系统事件的时候会放弃CPU。例如,一个进程可能不得不等待从一个文件中读取一个字符。这个等待发生在系统态的系统调用中。进程使用了库函数打开并读文件,库函数又执行系统调用从打开的文件中读入字节。这时,等候的进程会被挂起,另一个更加值得的进程将会被选择执行。进程经常调用系统调用,所以经常需要等待。即使进程执行到需要等待也有可能会用去不均衡的CPU事件,所以Linux使用抢先式的调度。用这种方案,每一个进程允许运行少量一段时间,200毫秒,当这个时间过去,选择另一个进程运行,原来的进程等待一段时间直到它又重新运行。这个时间段叫做时间片。
需要调度程序选择系统中所有可以运行的进程中最值得的进程。一个可以运行的进程是一个只等待CPU的进程。Linux使用合理而简单的基于优先级的调度算法在系统当前的进程中进行选择。当它选择了准备运行的新进程,它就保存当前进程的状态、和处理器相关的寄存器和其他需要保存的上下文信息到进程的task_struct数据结构中。然后恢复要运行的新的进程的状态(又和处理器相关),把系统的控制交给这个进程。为了公平地在系统中所有可以运行(runnable)的进程之间分配CPU时间,调度程序在每一个进程的task_struct结构中保存了信息。
policy
进程的调度策略:Linux有两种类型的进程:普通和实时。实时进程比所有其它进程的优先级高。如果有一个实时的进程准备运行,那么它总是先被运行。实时进程有两种策略:环或先进先出(round
robin and first in first
out)。在环的调度策略下,每一个实时进程依次运行,而在先进先出的策略下,每一个可以运行的进程按照它在调度队列中的顺序运行,这个顺序不会改变。
Priority 进程的调度优先级。也是它允许运行的时候可以使用的时间量(jiffies)。你可以通过系统调用或者renice命令来改变一个进程的优先级。
Rt_priority
Linux支持实时进程。这些进程比系统中其他非实时的进程拥有更高的优先级。这个域允许调度程序赋予每一个实时进程一个相对的优先级。实时进程的优先级可以用系统调用来修改Coutner
这时进程可以运行的时间量(jiffies)。进程启动的时候等于优先级(priority),每一次时钟周期递减。
调度程序schedule()从核心的多个地方运行。它可以在把当前进程放到等待队列之后运行,也可以在系统调用之后进程从系统态返回进程态之前运行。需要运行调度程序的另一个原因是系统时钟刚好把当前进程的计数器(counter)置成了0。每一次调度程序运行它做以下工作:
(1)kernel work 调度程序运行bottom half handler并处理系统的调度任务队列。
(2)Current pocess 在选择另一个进程之前必须处理当前进程。
(3)如果当前进程的调度策略是环则它放到运行队列的最后。
(4)如果任务状态是TASK_INTERRUPTIBLE的而且它上次调度的时候收到过一个信号,它的状态变为TASK_RUNNING;
如果当前进程超时,它的状态成为RUNNING;
如果当前进程的状态为RUNNING则保持此状态;
不是RUNNING或者INTERRUPTIBLE的进程被从运行队列中删除。这意味着当调度程序查找最值得运行的进程时不会考虑这样的进程。
(5)Process Selection
调度程序查看运行队列中的进程,查找最值得运行的进程。如果有实时的进程(具有实时调度策略),就会比普通进程更重一些。普通进程的重量是它的counter,但是对于实时进程则是counter
加1000。这意味着如果系统中存在可运行的实时进程,就总是在任何普通可运行的进程之前运行。当前的进程,因为用掉了一些时间片(它的counter减少了),所以如果系统中由其他同等优先级的进程,就会处于不利的位置:这也是应该的。如果几个进程又同样的优先级,最接近运行队列前段的那个就被选中。当前进程被放到运行队列的后面。如果一个平衡的系统,拥有大量相同优先级的进程,那么回按照顺序执行这些进程。这叫做环型调度策略。不过,因为进程需要等待资源,它们的运行顺序可能会变化。
(6)Swap Processes
如果最值得运行的进程不是当前进程,当前进程必须被挂起,运行新的进程。当一个进程运行的时候它使用了CPU和系统的寄存器和物理内存。每一次它调用例程都通过寄存器或者堆栈传递参数、保存数值比如调用例程的返回地址等。因此,当调度程序运行的时候它在当前进程的上下文运行。它可能是特权模式:核心态,但是它仍旧是当前运行的进程。当这个进程要挂起时,它的所有机器状态,包括程序计数器(PC)和所有的处理器寄存器,必须存到进程的task_struct数据结构中。然后,必须加载新进程的所有机器状态。这种操作依赖于系统,不同的CPU不会完全相同地实现,不过经常都是通过一些硬件的帮助。
(7)交换出去进程的上下文发生在调度的最后。前一个进程存储的上下文,就是当这个进程在调度结束的时候系统的硬件上下文的快照。相同的,当加载新的进程的上下文时,仍旧是调度结束时的快照,包括进程的程序计数器和寄存器的内容。
(8)如果前一个进程或者新的当前进程使用虚拟内存,则系统的页表需要更新。同样,这个动作适合体系结构相关。Alpha
AXP处理器,使用TLT(Translation Look-aside Table)或者缓存的页表条目,必须清除属于前一个进程的缓存的页表条目。
下面我就来总结一下进程创建以后到被杀死的整个进程生命周期中,状态可能在TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE
、TASK_STOPPED以及TASK_ZOMBLE之间转换的原因。
进程在TASK_RUNNING以及TASK_UNINTERRUPTIBLE、TASK_INTERRUPTIBLE之间转换:
获得CPU而正在运行的进程会由于某些原因,比如:申请不到某个资源,其状态会从TASK_RUNNING变为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE的等待状态。同样在经历了某些情况,处于等待状态的进程会被重新唤醒,等待分配给CPU。状态为TASK_INTERRUPTIBLE的睡眠进程会被唤醒,回到TASK_RUNNING状态,重新等待schedule()分配给它CPU,继续运行,比如:当申请资源有效时,也可以由signal或定时中断唤醒。而状态为TASK_INTERRUPTIBLE的睡眠进程只有当申请资源有效时被唤醒,不能被signal、定时中断唤醒。
1.通过sleep_on()、interruptible_sleep_on()、sleep_on_timeout()、interruptible_sleep_on_timeout()以及wake_up()、wake_up_process()、wake_up_interruptible()函数对进行的转换:
sleep_on():TASK_RUNNING->TASK_UNINTERRUPTIBLE
当拥有CPU的进程申请资源无效时,会通过sleep_on(),将进程从TASK_RUNNING切换到TASK_UNINTERRUPTIBLE状态。sleep_on()函数的作用就是将current进程的状态置成TASK_UNINTERRUPTIBLE,并加到等待队列中。
一般来说引起状态变成TASK_UNINTERRUPTIBLE的资源申请都是对一些硬件资源的申请,如果得不到这些资源,进程将不能执行下去,不能由signal信号或时钟中断唤醒,而回到TASK_RUNNING状态。
我们总结了这种类型的转换原因有:
(1)对某些资源的操作只能由一个进程进行,所以系统对该项资源采用上锁机制。在申请该项资源时,必须先申请资源的锁,如果已经被别的进程占用,则必须睡眠在对该锁的等待队列上。而且这种睡眠不能被中断,必须等到得到了资源才能继续进行下去。
如:
对网络连接表锁(Netlink table lock)的申请, sleep_on(&nl_table_wait);
对交换页进行I/O操作的锁的申请, sleep_on(&lock_queue);
对Hash表操作的锁的申请, sleep_on(&hash_wait);
在UMSDOS文件系统创建文件或目录时,必须等待其他同样的创建工作结束,sleep_on (&dir->u.umsdos_i.u.dir_info.p);
(2)某些进程在大部分时间处于睡眠状态,仅在需要时被唤醒去执行相应的操作,当执行完后,该进程又强制去睡眠。
如:
wakeup_bdflush()是对dirty buffer进行动态的响应,一旦该进程被激活,就将一定数量的dirty
buffer写回磁盘,然后调用sleep_on(&bdflush_done),又去睡眠。
interruptible_sleep_on():TASK_RUNNING->TASK_INTERRUPTIBLE
与sleep_on()函数非常地相象,当拥有CPU的进程申请资源无效时,会通过interruptible_sleep_on(),将进程从TASK_RUNNING切换到TASK_INTERRUPTIBLE状态。interruptible_sleep_on()函数的作用就是将current进程的状态置成TASK_INTERRUPTIBLE,并加到等待队列中。
处于TASK_INTERRUPTIBLE状态的进程可以在资源有效时被wake_up()、wake_up_interruptible()或wake_up_process()唤醒,或收到signal信号以及时间中断后被唤醒。
进行这种转换的原因基本上与sleep_on()相同,申请资源无效时进程切换到等待状态。与之不同的是处于interruptible_sleep_on()等待状态的进程是可以接受信号或中断而重新变为running状态。所以可以认为对这些资源的申请没有象在sleep_on()中资源的要求那么严格,必须得到该资源进程才能继续其运行下去。
sleep_on_timeout():TASK_RUNNING->TASK_UNINTERRUPTIBLE
sleep_on_timeout(&block.b_wait, 30*HZ);
interruptible_sleep_on_timeout():TASK_RUNNING->TASK_INTERRUPTIBLE
虽然在申请资源或运行中出现了某种错误,但是系统仍然给进程一次重新运行的机会。调用该函数将进程从TASK_RUNNING切换到TASK_INTERRUTIBLE状态,并等待规定的时间片长度,再重新试一次。
如:在smb_request_ok
中产生了连接失败的错误,会在sem_retry()中给一次重新连接的机会。//interruptible_sleep_on_timeout(&server->wait,
5*HZ);
wake_up():TASK_UNINTERRUPTIBLE-> TASK_RUNNING;
TASK_INTERRUPTIBLE-> TASK_RUNNING
处于TASK_UNINTERRUPTIBLE状态的进程不能由signal信号或时钟中断唤醒,只能由wake_up()或wake_up_process()唤醒。wake_up()函数的作用是将wait_queue中的所有状态为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE的进程状态都置为TASK_RUNNING,并将它们都放到running队列中去,即唤醒了所有等待在该队列上的进程。
void wake_up(struct wait_queue **q)
{
struct wait_queue *next;
struct wait_queue *head;
if (!q || !(next = *q))
return;
head = WAIT_QUEUE_HEAD(q);
while (next != head) {
struct task_struct *p = next->task;
next = next->next;
if (p != NULL) {
if ((p->state == TASK_UNINTERRUPTIBLE) ||
(p->state == TASK_INTERRUPTIBLE))
wake_up_process(p);
}
if (!next)
goto bad;
}
return;
bad:
printk("wait_queue is bad (eip = %p)\n",
__builtin_return_address(0));
printk(" q = %p\n",q);
printk(" *q = %p\n",*q);
}
wake_up()在下列情况下被调用:
这个函数通常在资源有效时调用,资源锁已经被释放,等待该资源的所有进程都被置为TASK_RUNNING状态,移到run
queue,重新参与调度,对这一资源再次竞争。这时又
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -