个人学习整理笔记,不保证正确性。。。
1、表示一个进程需要哪些信息
当进程在内存中时,内核要能够追踪到进程的状态,需要了解进程的各个段在内存中的分布,代码段、数据段、用户堆栈、核心堆栈等相关数据,还有进程执行是相关的寄存器。8个通用寄存器、段寄存器、指令寄存器等。进程的运行级别、进程的状态、进程使用的资源、进程相关的信号等等,内核需要有方法能够追踪到上述所有的数据,才能够保证对进程状态的全面掌控。
2、linux进程相关的数据结构
首先来看内核描述进程所用的数据结构。在Linux内核0.11版本中,进程数据结构的定义在sched.h文件中,定义如下
struct
task_struct {
/* these are hardcoded - don't touch */
long
state;
/*表示进程当前的状态的字段。关于进程状态字段相关值的定义也在shced.h文件中*/
long
counter; /*进程已用的时间片计数器*/
long
priority; /*进程优先级*/
long
signal; /*进程接收到的信号量*/
struct
sigaction sigaction[32]; /*sigaction与进程信号量的处理有关*/
long
blocked;
/* bitmap of masked signals */
/* various fields */
int
exit_code;
unsigned
long
start_code,end_code,end_data,brk,start_stack;
long
pid,father,pgrp,session,leader;
unsigned
short
uid,euid,suid;
unsigned
short
gid,egid,sgid;
long
alarm;
long
utime,stime,cutime,cstime,start_time;
unsigned
short
used_math;
/* file system info */
int
tty;
/* -1 if no tty, so it must be signed */
unsigned
short
umask;
struct
m_inode * pwd;
struct
m_inode * root;
struct
m_inode * executable;
unsigned
long
close_on_exec;
struct
file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct
desc_struct ldt[3];
/* tss for this task */
struct
tss_struct tss;
};
上面对task_struct的定义,又涉及到许多其它的数据结构,这里暂时就不一一列举,随着后面讨论的深入,会逐渐在相关的部分列出来
3、Linux进程状态state可选值有哪些与对state字段的修改
参见笔记“Linux进程状态变化详解”
4、进程的时间片计数器counter的初始值设定和修改
对进程couter字段初始值的设定是在fork.c中,但创建一个进程的时候,需要设定它的couter的初始值。couter的初始值就是进程的优先级数。具体的表示代码为(fork.c的copy_process函数中,这个函数是创建子进程赋值父进程的数据时使用)
p->counter = p->priority;具体的进程的初始化将在后面讨论。对counter的修改是在两个地方,一个是在sched.c的schedule函数中,这里的修改主要是在进行进程调度时找不到合适的进程去调度(要么处于睡眠,要么counter值已经为0了),就把所有进程的counter的值都进行修改,这个地方修改counter的代码如下
for
(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if
(*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
还有一个地方是在处理中断函数中do_timer里面,将当前进程的时间片减1。代码如下
if
((--current->counter)>0)
return
;
current->counter=0;
5、进程优先级的初始值与优先级调整
进程的优先级的初始值是设置与其父进程一样,在fork.c中copy_process中,有这么一句*p=*current;然后,在后面剩下的部分,没有对p的priority字段做单独的设置,所以一个进程的优先级是与创建这个进程的父进程的优先级是一样的。
对进程优先级做修改的一个函数是sched.c的sys_nice,这个函数的定义如下
int
sys_nice(
long
increment)
{
if
(current->priority-increment>0)
current->priority -= increment;
return
0;
}
从函数的定义来看,这个函数的作用就是根据传入的值,来对进程的优先级进行调整,是进程的优先级降低。而sys_nice被放在了系统调用表中sys_call_table表中
6、进程信号量
常见另一篇笔记“Linux信号系统详解”
7、进程变量
exit_code表示进程退出时代码值,在进程退出时会根据调用exit传递的参数来设置exit_code的值
[进程的代码和数据相关]
start_code、end_code、end_data、brk、start_stack表示的是进程中的代码段、数据段、栈等信息在内存中的分布。其中start_code表示代码段和数据段的基地址(在Linux 0.11中,代码段和数据段共用一个段),在创建新进程的时候会设置进程的start_code的值,是一个线性地址。
end_code表示代码段的长度,进程的end_code的设置在当使用exec系统调用执行新的程序的时候,会设定当前进程的代码段、数据段和堆栈段的信息。在Linux 0.11中设定这个值的代码如下(在exec.c的do_execve中)
current->brk = ex.a_bss +
(current->end_data = ex.a_data +
(current->end_code = ex.a_text));
current->start_stack = p & 0xfffff000;
ex是一个struct exec类型的变量.struct exec的定义在a.out.h中,定义如下
struct
exec {
unsigned
long
a_magic;
/* Use macros N_MAGIC, etc for access */
unsigned
a_text;
/* length of text, in bytes */
unsigned
a_data;
/* length of data, in bytes */
unsigned
a_bss;
/* length of uninitialized data area for file, in bytes */
unsigned
a_syms;
/* length of symbol table data in file, in bytes */
unsigned
a_entry;
/* start address */
unsigned
a_trsize;
/* length of relocation info for text, in bytes */
unsigned
a_drsize;
/* length of relocation info for data, in bytes */
};
因此将上述的代码分解之后也就是
end_code=a_text; 表示代码段的长度
end_data=a_data+a_text; 表示数据段的长度,只不过在Linux 0.11中,代码和数据段合并为一个段,统称为数据段了。
所以这里的end_data的值包括了end_code的部分
brk=a_bss+a_data+a_text; 进程堆的结尾字段(动态内存分配部分),brk表示进程动态堆之前的所有长度,也就是可以将brk理解成堆的开始位置
start_stack=p&0xffff000设置进程的栈的起始地址。其中p的定义为
unsigned
long
p=PAGE_SIZE*MAX_ARG_PAGES-4; PAGE_SIZE表示每一个内存页的大小,MAX_ARG_PAGES为新程序分配给参数和环境变量使用的内存最大页数。那么p就表示的是分配给调用进程的参数和环境变量的最大内存值,这0.11中,p的值为128KB。这样之后,就让start_stack指向了参数的下一页(-4保证了start_stack不会指向参数的最后一页,而是指向下一页)。
对于bss部分,进程在执行新的程序的时候,会预先将一页内存数据设为0,操作的代码如下
i = ex.a_text+ex.a_data;
while
(i&0xfff)
put_fs_byte(0,(
char
*) (i++));
这里个人认为可以把i理解成一个逻辑地址。执行完成之后,进程的逻辑地址空间示意图如下(具体与文件相关的细节后续另外讨论)
[进程的id、进程组和会话相关]
long
pid,father,pgrp,session,leader;进程的id,父进程和分组信息相关。pid表示的是进程的id。father表示进程的父进程的pid。pgrp表示进程所在的进程组的组号,它的值与进程组的组长进程的pid相同。session表示进程所处的会话组。leader表示进程是否是当前进程组的组长进程,leader为1表示当前进程是进程组的组长进程,否则不是。一个进程属于某个进程组,每次登陆就是一次会话,会话由多个进程组构成。
对于非进程组组长的进程,通过调用sys.c的sys_setpgid可以改表其所属的进程组,但组长进程不能改变其所属的进程组。
对于非进程组组长的进程,通过调用sys.c的sys_setsid可以创建新的会话,创建的新会话中,session=pid=pgid,但组长进程不能创建新的会话
[进程的用户相关]
unsigned
short
uid,euid,suid;分别表示进程的用户id、进程的有效用户id和保存的有效用户id。
unsigned
short
gid,egid,sgid;分别对应uid、euid和suid,表示
在《Linux内核源代码情景分析》中的解释如下。
除用户名外,每个用户还被授予一个唯一的用户号,即uid。每个进程都属于某一个用户,因此有一个对应的uid值。有时候需要改变进程的用户号来改变其访问权限,因此引申出了进程的有效用户号,effective uid,即euid,并且提供了改变进程的euid的系统调用。考虑到当进程的有效用户号发生变化后可能需要后续恢复这个有效用户号,这样就提供了一个saved uid,即suid来保存这个被改变的有效用户号。
每一个用户有一个唯一的用户号外,还属于某一个用户组,用户所属的这个用户组用gid来表示。类似的,当改变了用户的id时,用户所属的用户组也可能要发生变化,即对进程来说其有效的用户组发生了变化,这个有效的用户组就用egid来表示。当改变了有效用户组时,之前的有效用户组要保存起来,用sgid来表示。
[进程的定时器相关]
long
alarm;当alarm不为0时用来表示进程在多长时间之后会收到定时器的消息。alarm的值与内核的另一个变量jiffies紧密相关。jiffies为Linux的内核变量数据,它用来记录系统自开机以来已经经过了多少的滴答(tick),每发生一次时钟中断,就表示一个tick,就会将jiffies的值加1。系统发生中断的频率由定义的宏HZ来控制,HZ表示一秒内发生中断的次数。在0.11中这个值是100,即每10ms发生一次时钟中断。
例如在sched.c中的sys_alarm中设置进程在多少秒后产生报警信息,代码如下
int
sys_alarm(
long
seconds)
{
int
old = current->alarm;
if
(old)
old = (old - jiffies) / HZ;
current->alarm = (seconds>0)?(jiffies+HZ*seconds):0;
return
(old);
}
int old=current->alarm;首先获得旧的定时器的值。因为jiffies代表的是一个tick次数,所以设定进程的定时报警信息时会把传入的时间(单位是秒)转换成相应的滴答值。同理,旧的alarm值也要被还原成相应的时间(单位是秒),不过还原的值并不是上一次设定的报警时间,而是与当前时间的差值。
所以
if(old) old=(old-jiffies)/HZ;将旧的定时器的值还原成时间。
current->alarm=(seconds>0)?(jiffies+HZ*seconds):0;将传入的报警时间设置成相应的滴答值。即从现在开始算起,经过HZ*seconds次滴答值后,对进程产生报警信息。产生报警信息的代码位于sched.c的schedule函数中,在这个函数的最开始的部分,代码如下
/* check alarm, wake up any interruptible tasks that have got a signal */
for
(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if
(*p) {
if
((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
alarm的值不为0,并且alarm<jiffies表示当前的时间已经到达了设置的报警时间了。
[进程运行时间相关]
long
utime,stime,cutime,cstime,start_time;与进程的各个级别下的运行时间相关。
utime表示进程在用户态下的运行时间(滴答数表示),stime表示进程在核心态(系统态)下的运行时间(滴答数表示)。在时钟中断的处理函数do_timer(sched.c)中,有如下部分代码
if
(cpl)
current->utime++;
else
current->stime++;
cpl表示当前的进程状态,0.11中cpl只有两个值,0表示系统态,3表示用户态。这里就是根据进程当前的运行状态,来统计进程的运行时间。cstime和cutime分别表示这个进程的子进程在系统态和用户态下的运行时间的总和(滴答数表示)。当一个进程调用waitpid来回收子进程信息时,会将子进程的运行时间分别累加到父进程中(在exit.c的sys_waitpid中有相应的代码)。
[协处理器相关]
unsigned
short
used_math;用来表示进程是否需要使用到了协处理器,used_math为1表示使用了协处理器,为0表示没有使用协处理器
[文件和终端相关]
文件系统部分得单独做次笔记
int
tty;进程使用的终端号,-1时表示进程没有使用任何终端
unsigned
short
umask;对应文件的屏蔽码,用于文件的权限管理。这是对于文件访问权限的屏蔽位,格式与文件访问权限相同。umask的作用是将相应位上的文件访问权限屏蔽掉,使其不发生作用。在Linux中文件权限的描述格式是u g o,分别对应文件所有者(user)、文件所有者所在的组(group)、其他人(other)。
文件权限mode的格式如下,对user、group和other每一个角色都分别设定了读、写和执行的权限,用八进制表示的示意图如上所示。当我们设置了umask的值后,在打开文件时,会根据设置的umask的值对mode进行操作。在open.c的sys_open中有相应的代码
int
sys_open(
const
char
* filename,
int
flag,
int
mode)
{
struct
m_inode * inode;
struct
file * f;
int
i,fd;
mode &= 0777 & ~current->umask;
mode是传递的文件权限的描述,为了屏蔽相应的位,先对umask取反,然后与mode作与运算。比如说,如果将umask的值设为077(八进制),也就是
000 111 111,取反之后变为111 000 000,再与mode做与运算值后,将group和other角色的权限全部给屏蔽掉了。
struct
m_inode * pwd;
struct
m_inode * root;
struct
m_inode * executable;与进程的目录相关。pwd为当前工作目录i节点结构,root为根目录i节点结构,executable为执行文件i节点结构
unsigned
long
close_on_exec;执行时关闭文件句柄位图标志。简单的理解就是,在当前进程通过exec执行一个新的程序时,需要关闭的文件。如果第i位为1,则表示需要将文件描述符为i对应的文件关闭。在exec.c的do_execve中,有以下部分代码
for
(i=0 ; i<NR_OPEN ; i++)
if
((current->close_on_exec>>i)&1)
sys_close(i);
current->close_on_exec = 0;
NR_OPEN是一个定义的宏,值为20.即在0.11中,一个进程最多能打开20个文件。首先通过(close_on_exec>>i)&1,看close_on_exec的第i位是否为1,如果是的,就调用sys_close(i),关闭相应的文件。所有需要关闭的文件都关闭之后,将close_on_exec的值设为0.
struct
file * filp[NR_OPEN];进程所有使用的文件数组。
[进程的局部段描述符表相关]
struct
desc_struct ldt[3];在内存管理和地址翻译部分说明了段描述符表的结构和作用。
ldt[0]没有使用,ldt[1]表示代码段的描述符,ldt[2]为数据段的描述符
在0.11中,进程在内存地址空间在逻辑分为了代码段和数据段两部分,但是在实际上代码段和数据段使用的是同一个段基地址,即这两个段的起始地址相同。在fork调用的时候,子进程会复制父进程的页表数据,同时设置自己的局部段描述符表。有如下部分代码(来自exec.c的copy_mem函数),正好对应在进程变量部分的图示说明
new_data_base = new_code_base = nr * 0x4000000;
//0x4000000=2^26
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
设置段的基地址的时候,ldt[1]和ldt[2]中的段的基地址都是nr*0x4000000。
在exec.c的change_ldt中也有相应的代码,摘抄部分如下。
static
unsigned
long
change_ldt(
unsigned
long
text_size,
unsigned
long
* page)
{
unsigned
long
code_limit,data_limit,code_base,data_base;
int
i;
code_limit = text_size+PAGE_SIZE -1;
code_limit &= 0xFFFFF000;
data_limit = 0x4000000; //64M
code_base = get_base(current->ldt[1]);
data_base = code_base;
set_base(current->ldt[1],code_base);
set_limit(current->ldt[1],code_limit);
set_base(current->ldt[2],data_base);
set_limit(current->ldt[2],data_limit);
这个函数在do_execve中被调用,即进程执行新的程序的时候,会需要修改进程的局部段描述符表。如代码所示,code_base=data_base
但是这两个段的段长是不一样的。
[进程的任务状态信息]
struct
tss_struct tss;表示进程执行时的状态信息。在tss中存储了进程在执行时相关的寄存器等信息。struct tss_struct定义如下
struct
tss_struct {
long
back_link;
/* 16 high bits zero */
long
esp0;
long
ss0;
/* 16 high bits zero */
long
esp1;
long
ss1;
/* 16 high bits zero */
long
esp2;
long
ss2;
/* 16 high bits zero */
long
cr3;
long
eip;
long
eflags;
long
eax,ecx,edx,ebx;
long
esp;
long
ebp;
long
esi;
long
edi;
long
es;
/* 16 high bits zero */
long
cs;
/* 16 high bits zero */
long
ss;
/* 16 high bits zero */
long
ds;
/* 16 high bits zero */
long
fs;
/* 16 high bits zero */
long
gs;
/* 16 high bits zero */
long
ldt;
/* 16 high bits zero */
long
trace_bitmap;
/* bits: trace 0, bitmap 16-31 */
struct
i387_struct i387;
};