Linux进程管理代码分析文档格式.docx
《Linux进程管理代码分析文档格式.docx》由会员分享,可在线阅读,更多相关《Linux进程管理代码分析文档格式.docx(46页珍藏版)》请在冰点文库上搜索。
简单的。
简单有序
这样的程序对程序员来说不亚于管理一班人,程序员为每个进程设计好相应的功能,并
通过一定的通讯机制将它们有机地结合在一起,对每个进程的设计是简单的,只在总控部分
小心应付(其实也是蛮简单的),就可完成整个程序的施工。
互不干扰
这个特点是操作系统的特点,各个进程是独立的,不会串位。
事务化
比如在一个数据电话查询系统中,将程序设计成一个进程只处理一次查询即可,即完成
一个事务。
当电话查询开始时,产生这样一个进程对付这次查询;
另一个电话进来时,主控程序又产生一个这样的进程对付,每个进程完成查询任务后消失•这样的编程多简单,只要
做一次查询的程序就可以了。
Linux是一个多进程的操作系统,进程是分离的任务,拥有各自的权利和责任。
如果一
个进程崩溃,它不应该让系统的另一个进程崩溃。
每一个独立的进程运行在自己的虚拟地址
空间,除了通过安全的核心管理的机制之外无法影响其他的进程。
在一个进程的生命周期中,进程会使用许多系统资源。
比如利用系统的CPU执行它的指
令,用系统的物理内存来存储它和它的数据。
它会打开和使用文件系统中的文件,会直接或
者间接使用系统的物理设备。
如果一个进程独占了系统的大部分物理内存和CPU对于其他
进程就是不公平的。
所以Linux必须跟踪进程本身和它使用的系统资源以便公平地管理系统中的进程。
系统最宝贵的资源就是CPU通常系统只有一个CPULinux作为一个多进程的操作系统,
它的目标就是让进程在系统的CPU上运行,充分利用CPU如果进程数多于CPU(—般情况
都是这样),其他的进程就必须等到CPU被释放才能运行。
多进程的思想就是:
一个进程一
直运行,直到它必须等待,通常是等待一些系统资源,等拥有了资源,它才可以继续运行。
在一个单进程的系统中,比如DOSCPU被简单地设为空闲,这样等待资源的时间就会被浪
费。
而在一个多进程的系统中,同一时刻许多进程在内存中,当一个进程必须等待时,操作
系统将CPU从这个进程切换到另一个更需要的进程。
我们组分析的是Linux进程的状态转换以及标志位的作用,它没有具体对应某个系统调用,而是分布在各个系统调用中。
所以我们详细而广泛地分析了大量的原码,对进程状态转
换的原因、方式和结果进行了分析,大致总结了整个Linux系统对进程状态管理的实现机制。
Linux中,每个进程用一个task_struct的数据结构来表示,用来管理系统中的进程。
Task向量表是指向系统中每一个task_struct数据结构的指针的数组。
这意味着系统中的
最大进程数受到Task向量表的限制,缺省是512。
这个表让Linux可以查到系统中的所有
的进程。
操作系统初始化后,建立了第一个task_struct数据结构INIT_TASK。
当新的进程
创建时,从系统内存中分配一个新的task_struct,并增加到Task向量表中。
为了更容易
查找,用current指针指向当前运行的进程。
task_struct结构中有关于进程调度的两个重要的数据项:
structtask_struct{
volatilelongstate;
/*-1unrunnable,0runnable,>
0stopped*/
unsignedlongflags;
/*perprocessflags,definedbelow*/
};
每个在Task向量表中登记的进程都有相应的进程状态和进程标志,是进行进程调度的重
要依据。
进程在执行了相应的进程调度操作后,会由于某些原因改变自身的状态和标志,也
就是改变state和flags这两个数据项。
进程的状态不同、标志位不同对应了进程可以执行不同操作。
在Linux2.2.8版本的sched.h中定义了六种状态,十三种标志。
//进程状态
#defineTASK_RUNNING0
#defineTASK_INTERRUPTIBLE1
#defineTASK_UNINTERRUPTIBLE2
#defineTASK_ZOMBIE4
#defineTASK_STOPPED8
#defineTASK_SWAPPING16
它们的含义分别是:
TASK_RUNNING正在运行的进程(是系统的当前进程)或准备运行的进程(在Running
队列中,等待被安排到系统的CPU。
处于该状态的进程实际参与了进程调度。
TASK_INTERRUPTIBLE处于等待队列中的进程,待资源有效时唤醒,也可由其它进程被
信号中断、唤醒后进入就绪状态。
TASK_UNINTERRUPTIBLE处于等待队列中的进程,直接等待硬件条件,待资源有效时唤醒,不可由其它进程通过信号中断、唤醒。
TASK_ZOMBIE终止的进程,是进程结束运行前的一个过度状态(僵死状态)。
虽然此时
已经释放了内存、文件等资源,但是在Task向量表中仍有一个task_struct数据结构项。
它不进行任何调度或状态转换,等待父进程将它彻底释放。
TASK_STOPPEDt程被暂停,通过其它进程的信号才能唤醒。
正在调试的进程可以在该
停止状态。
TASK_SWAPPINGS程页面被兑换出内存的进程。
这个状态基本上没有用到,只有在
sched.c的count_active_tasks()函数中判断处于该种状态的进程也属于active的进
程,但没有对该状态的赋值。
//进程标志位:
#definePF_ALIGNWARN0x00000001
#definePF_STARTING0x00000002
#definePF_EXITING0x00000004
#definePF_PTRACED0x00000010
#definePF_TRACESYS0x00000020
#definePF_FORKNOEXEC0x00000040
#definePF_SUPERPRIV0x00000100
#definePF_DUMPCORE0x00000200
#definePF_SIGNALED0x00000400
#definePF_MEMALLOC0x00000800
#definePF_VFORK0x00001000
#definePF_USEDFPU0x00100000
#definePF_DTRACE0x00200000
其中PF_STARTING^有用到。
PF_MEMEALLOCPF_VFOR这两个标志位是新版本中才有的。
各个标志位的代表着不同含义,对应着不同调用:
1、PF_ALIGNWARN标志打印“对齐”警告信息,只有在486机器上实现
2、PF_STARTING进程正被创建
3、PF_EXITING标志进程开始关闭。
在do_exit()时置位。
current->
flags|=PF_EXITING
用于判断是否有效进程。
在nImclnt_proc()(在fs\lockd\clntproc.c),如果current_flag为PF_EXITING,
则进程由于正在退出清除所有的锁,将执行异步RPC调用。
4、PF_PTRACED进程被跟踪标志,
在do_fork()时清位。
p->
flags&
=~PF_PTRACED
当ptrace(0)被调用时置位,在进程释放前要清掉。
current->
flags|=PF_PTRACED
在sys_trace()中判断
如果request为PTRACE_TRACE,E口是则将current_flag置为PF_PTRACED
如果request为PTRACE_ATTACH则将child_flag置为PF_PTRACEJD给child发一个SIGSTOP信号;
如果request为PTRACE_DETACH则将child清除PF_PTRACED
在syscall_trace()中判断current_flag如果为PF_TRACED^PF_TRACESY则
current强行退出时的出错代码置为SIGTRAP并将状态置为STOPPED
5、PF_TRACESYS正在跟踪系统调用。
do_fork()时清位,在进程释放前要清掉。
在sys_trace()中判断request如果为PTRACE_SYSCALL则将child->
flags置为PF_TRACESYS如为PTRACE_SYSCALL则将child->
flags清除PF_TRACESY;
然后唤醒child。
如果request为PTRACE_SINGLESTE即单步跟踪),则将child_flag清除PF_TRACESY唤醒child。
6、PF_FORKNOEXEC进程刚创建,但还没执行。
在do_fork()时置位。
flags|=PF_FORKNOEXEC
在调入格式文件时清位。
=~PF_FORKNOEXEC
7、PF_SUPERPRIV超级用户特权标志。
如果是超级用户进程则置位,用户特权设为超级用户,如是超级用户,在统计时置
统计标志(accountingflag)为ASU
8、PF_DUMPCORE标志进程是否清空core文件。
Core文件由gdb进行管理,给用户提供有用信息,例如查看浮点寄存器的内容比较困难,事实上我们可以从内核文件里的用户结构中得到
Core文件格式如下图:
~Upage
飞ATA
STACK
Core文件结构
UPAGE是包含用户结构的一个页面,告诉gdb文件中现有内容所有寄存器也在
UPAG冲,通常只有一页。
DATA存放数据区。
STACK堆栈区
最小Core文件长度为三页(12288字节)
在task_struct中定义一个dumpable变量,当dumpable==1时表示进程可以清空core文件(即将core文件放入回收站),等于0时表示该进程不能清空core文件
(即core文件以放在回收站中,不可再放到回收站中),此变量初值为1。
例如在调用doaoutcoredump()时判断current—>
dumpable是否等于((即判断
该进程是否能将core文件放入回收站),如果等于1则将该变量置为0,在当前目录下建立一个coredumpimage,在清空用户结构前,由gdb算出数据段和堆栈段的位置和使用的虚地址,用户数据区和堆栈区在清空前将相应内容写入coredump
将PF_DUMPCOI置位,清空数据区和堆栈区。
只有在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()
10、PF_USEDFPU标志该进程使用FPU此标志只在SMP时使用。
在task_struct中有一变量used_math,进程是否使用FPU。
在CPU从prev切换到next时,如果prev使用FPU则prev的flag清除PF_USEDFP。
prev->
flags&
=~PF_USEDFPU
在flush_thread()(arch\i386\kernel\process.c)、restore_i387_hard()、
save_i387_hard()(arch\i386\kernel\signal.c)中,如果是SMP方式,且使用FPU
贝Ustts(),否则清除PF_USEDFPUI
current->
=~PF_USEDFPU
在sys_trace()中如果request为PTRACE_SETFPREG则将child的used_math置为1,将child_flag清除PF_USEDFPU
child->
在SMP方式下进行跟踪时,判断是否使用FPU
在跟踪时出现数学错误时清位。
11、PF_DTRACE进程延期跟踪标志,只在m68k下使用。
跟踪一个trapping指令时置位。
flags|=PF_DTRACE
12、PF_ONSIGSTK标志进程是否工作在信号栈,只在m68k方式下使用。
liunx2.1.19版本中使用此标志位,而2.2.8版本中不使用。
在处理信号建立frame时如果sigaction标志为ONSTACK则将current->
flag置
为PF_ONSIGSTU
13、PF_MEMALLOC进程分配内存标志。
linux2.2.8版本中使用此标志位。
在kpiod()和kwpad()中置位。
tsk->
flags|=PF_MEMALLOC
14、PF_VFORKlinux2.2.8版本中使用此标志位。
在copy_flags(unsignedlongcione_flags,structtask_struct*p),如果
clone_flags为CLONE_VFORK贝将p的flags置为PF_VFORU
在mm_release()中将current->
flags清除PF_VFORK
=~PF_VFORK
具体的分析由我组的另外同学进行。
Linux的各进程之间的状态转换的系统调用
我将参与Linux的各进程之间的状态转换的系统调用总结成一张流程图:
1厂「T_j二u
i:
-1
I1TASKINTERRUPTIBL^ENGSTATU^aSKUNINTERRUPTIBLE
进程的创建:
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挂到就绪队列runqueue,返回子进程的pid。
//C:
\SRCLNX\KERNEL\FORK.C
intdo_fork(unsignedlongcione_flags,unsignedlongusp,structpt_regs*regs)
{
为新进程申请PCB空间;
if(申请不到)
返回错误,退出;
为新进程申请核心堆栈;
if(核心堆栈申请不到)
为新进程在Task向量表中找到空闲位置;
/*复制父进程currentPCB中的信息,继承current的资源*/;
p=current;
在进行正式初始化以前,将新创建的进程的状态都置为TASK_UNINTERRUPTIBLS^免
初始化过程被打断,并置一些标志位•
/*为防止信号、定时中断误唤醒未创建完毕的进程,将子进程的状态设成不可中断的*/
state=TASK_UNINTERRUPTIBLE;
/*跟踪状态和超级用户特权是没有继承性的,因为在root用户为普通用户创建进程时,出于
安全考虑这个普通用户的进程不允许拥有超级用户特权。
*/
=~(PF_PTRACED|PF_TRACESYS|PF_SUPERPRIV);
/*将进程标志设成初建,在进程第一次获得CPU寸,内核将根据此标志进行一定操作*/
flags|=PF_FORKNOEXEC;
开始Task_struct的初始化工作,如初始化进程时钟、信号、时间等数据;
继承父进程所有资源:
拷贝父进程当前打开的文件;
拷贝父进程在VFS的位置;
拷贝父进程的信号量;
拷贝父进程运行的内存;
拷贝父进程的线程;
初始化工作结束,父进程将其将其唤醒,挂入running队列中,返回子进程的pid;
进程的调度(schedule。
):
处于TASK_RUNNIN状态的进程移到runqueue,会由schedule()按CPU调度算法在合适的时候选中,分配给CPU
新创建的进程都是处于TASK_RUNNIN状态,而且被挂到runqueue的队首。
进程调度采用变形的轮转法(roundrobin)。
当时间片到时(10ms的整数倍),由时钟中断引起新一轮调度,把当前进程挂到runqueue队尾。
所有的进程部分运行与用户态,部分运行于系统态。
底层的硬件如何支持这些状态各不相同但是通常有一个安全机制从用户态转入系统态并转回来。
用户态比系统态的权限低了很
多。
每一次进程执行一个系统调用,它都从用户态切换到系统态并继续执行。
这时让核心执
行这个进程。
Linux中,进程不是互相争夺成为当前运行的进程,它们无法停止正在运行的其它进程然后执行自身。
每一个进程在它必须等待一些系统事件的时候会放弃CPU例如,
一个进程可能不得不等待从一个文件中读取一个字符。
这个等待发生在系统态的系统调用中。
进程使用了库函数打开并读文件,库函数又执行系统调用从打开的文件中读入字节。
这
时,等候的进程会被挂起,另一个更加值得的进程将会被选择执行。
进程经常调用系统调用,
所以经常需要等待。
即使进程执行到需要等待也有可能会用去不均衡的CPU事件,所以Linux
使用抢先式的调度。
用这种方案,每一个进程允许运行少量一段时间,200毫秒,当这个时
间过去,选择另一个进程运行,原来的进程等待一段时间直到它又重新运行。
这个时间段叫
做时间片。
需要调度程序选择系统中所有可以运行的进程中最值得的进程。
一个可以运行的进程是一个只等待CPU的进程。
Linux使用合理而简单的基于优先级的调度算法在系统当前的进程中进行选择。
当它选择了准备运行的新进程,它就保存当前进程的状态、和处理器相关的寄
存器和其他需要保存的上下文信息到进程的task_struct数据结构中。
然后恢复要运行的新
的进程的状态(又和处理器相关),把系统的控制交给这个进程。
为了公平地在系统中所有可以运行(runnable)的进程之间分配CPU时间,调度程序在每一个进程的task_struct
结构中保存了信息。
policy进程的调度策略:
Linux有两种类型的进程:
普通和实时。
实时进程比所有其它
进程的优先级高。
如果有一个实时的进程准备运行,那么它总是先被运行。
实时进程有两种
策略:
环或先进先出(roundrobinandfirstinfirstout)。
在环的调度策略下,每一
个实时进程依次运行,而在先进先出的策略下,每一个可以运行的进程按照它在调度队列中
的顺序运行,这个顺序不会改变。
Priority进程的调度优先级。
也是它允许运行的时候可以使用的时间量(jiffies)。
你
可以通过系统调用或者