最近在学习Linux 0.11内核源码,近几天主要阅读了进程的基本数据结构、进程的创建/加载运行/终结等代码,现对进程的生命周期进行总结和分享。欢迎大家批评指正,描述有误的地方请务必告知,非常感谢!说明:本文参考书目为《Linux内核设计的艺术》第二版和《Linux内核设计与实现》第三版。
围绕进程,本文内容的结构分布如下,其中第2,3,4,5部分均结合Linux 0.11源码进行分析。
章节 | 主要内容 |
---|---|
1. 进程的基本概念 | 进程、程序、任务 |
2. 进程相关的数据结构 | tast_struct结构体 |
3. 进程的创建 | sys_fork, find_empty_process(), copy_process() |
4. 进程的加载和运行 | do_execve(), do_no_page() |
5. 进程的终结 | do_exit() |
现有的进程相关的定义有很多,下边是一些可参考的描述:
进程的另一个名字是任务(task),Linux内核通常把进程也叫做任务。
在1.1中有定义提到进程包含的资源,这里详细列举一些具体资源:代码段,数据段,打开的文件,挂起的信号,内核内部数据,一个/多个具有内存映射的内存地址空间,一个或多个执行线程等(还有一些其他的资源,具体可参见2.1中task_struct结构体的成员变量)。
结构体task_struct中存放进程描述符(process descriptor)的内容,进程描述符中包含的数据能够完整的描述一个正在执行的程序:他打开的文件、进程的地址空间、挂起的信号、进程的状态等等。包含一个内核管理一个进程所需要的所有信息。
task_struct结构体代码位于\linux\sched.h文件,其代码如下:
state表示进程运行状态。counter为剩余的时间片,任务开始运行时,counter值等于priority的值,随着任务的运行,counter值不断递减。priority为进程优先级,该值越大,表明该进程优先级越高。signal为信号位图,每个比特位代表一个信号,信号值=位偏移值+1。sigaction为信号执行属性结构,对应信号的执行操作和标志信息。blocked是信号屏蔽码,与signal信号位图对应。exit_code是退出码,在进程退出时,该值会传递给其父进程。start_code为代码段地址。end_code为代码段长度的字节数。end_data为代码段长度+数据段长度的字节数。brk为总长度的字节数。start_stack为堆栈段地址。pid为该进程进程号。father为其父进程的进程号。pgrp为其父进程组号。session为会话号。leader为会话首领。uid/euid/suid分别为用户标识号、有效用户标识号、保存的用户标识号。gid/egid/sgid类似,为组标识号、有效组标识号、保存的组标识号。alarm为报警计时器。utime/stime分别为用户态运行时间和系统态运行时间。cutime/cstime类似,为子进程用户态运行时间和子进程系统态运行时间。start_time为进程开始运行时刻。used_math标识当前进程是否使用协处理器。tty是进程使用tty的子设备号。umask是文件创建属性屏蔽位。pwd/root/executable分别为当前工作目录节点、根目录节点、当前进程对应的可执行程序文件节点的结构。close_on_exec为执行时关闭文件句柄位图标志。filp[NR_OPEN]为当前进程使用的文件表结构。ldt[3]为本进程的局部表描述符,ldt[0]为空,ldt[1]为代码段,ldt[2]为数据段和堆栈段。tss描述了本进程的任务状态段信息结构。
sa_handler是对应某信号指定要采取的行动,是指向处理该信号函数的指针。sa_mask给出了对信号的屏蔽码,在信号处理程序执行时,将阻塞对这些信号的处理。sa_flags指定改变信号处理过程的信号集。sa_restorer恢复过程指针。
为内存中i节点的结构。i_mode为文件类型和属性(rwx位)。i_uid为文件拥有者标识符。i_size为文件大小。i_mtime为文件修改时间。i_gid为文件所有者所在组标识。i_nlinks为文件目录项链接数。i_zone[9]为直接(0-6)间接(7)或双重间接(8)逻辑块号。i_wait为等待该节点的进程。i_atime和i_ctime分别为最后访问时间、i节点自身修改时间。i_dev为i节点所在设备号。i_num为i节点号。i_count为i节点被使用的次数。i_lock/i_dirty分别为锁标志和修改(脏)标志。i_pipe和i_update分别为管道标志和更新标志。
用于在文件句柄与i节点之间建立关系。f_mode为文件操作模式(RW位)。f_flags为文件打开和控制的标志。f_count为对应文件句柄(文件描述符)数。f_inode指向对应的i节点。f_ops为文件位置,读写偏移。
desc_struct定义了段描述符的数据结构。每一个段描述符由8个字节构成。
tss_struct为任务状态段数据结构。其中包含进程运行过程中的寄存器信息、段信息等。
许多其他操作系统提供了产生(spawn)进程的机制。首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix将上述步骤分解到两个函数中执行:fork()和exec()。这里我们先介绍创建的过程,重点分析fork()。
# linux-0.11\kernel\system_call.s
.align 2
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
内核通过sys_fork系统调用的来实现进程的创建,其中调用find_empty_process()与copy_process()两个函数。
首先通过find_empty_process(),在执行repeat过程中取得一个进程号last_pid,接着在任务数组task[]中找到闲置任务下标i。这两个值之后会以传入到copy_process()中。
// linux-0.11\kernel\fork.c
int find_empty_process(void)
{
int i;
repeat:
if ((++last_pid)<0) last_pid=1;
for(i=0 ; i<NR_TASKS ; i++)
if (task[i] && task[i]->pid == last_pid) goto repeat;
for(i=1 ; i<NR_TASKS ; i++)
if (!task[i])
return i;
return -EAGAIN;
}
然后调用copy_process()复制进程。其详细流程如下:
// linux-0.11\kernel\fork.c
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority;
p->signal = 0;
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies;
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff;
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++)
if (f=p->filp[i])
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}
这里分为几个部分:
加载程序之前需要做一些准备工作,如准备进程参数和环境变量所占用的页面,检查可执行文件等。
加载子进程自己的程序,那么对于从父进程拷贝的资源,有些要解除,有些要清零,在这里需要进行个性化设置。
程序加载完成之后,会调整eip和esp使得子进程之后被运行。但子进程和父进程解除了共享页面的关系,控制页面的页表已经释放。这意味着页目录项的内容为0,包括P位也为0。那么子进程程序一开始执行,MMU解析线性地址值时就会发现对应的页目录项P位为0,因此产生缺页中断。
产生缺页中断并由操作系统响应。缺页中断信号产生后,page_fault这个服务程序将对此进行响应,并最终在_page_falut中通过调用call _do_no_page调用到缺页中断处理程序_do_no_page()中执行。
do_execve()函数负责读取可执行文件并将其载入地址空间开始运行。其详细流程如下:
int do_execve(unsigned long * eip,long tmp,char * filename,
char ** argv, char ** envp)
{
struct m_inode * inode;
struct buffer_head * bh;
struct exec ex;
unsigned long page[MAX_ARG_PAGES];
int i,argc,envc;
int e_uid, e_gid;
int retval;
int sh_bang = 0;
unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;
if ((0xffff & eip[1]) != 0x000f)
panic("execve called from supervisor mode");
for (i=0 ; i<MAX_ARG_PAGES ; i++) /* clear page-table */
page[i]=0;
if (!(inode=namei(filename))) /* get executables inode */
return -ENOENT;
argc = count(argv);
envc = count(envp);
restart_interp:
if (!S_ISREG(inode->i_mode)) {
/* must be regular file */
retval = -EACCES;
goto exec_error2;
}
i = inode->i_mode;
e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
if (current->euid == inode->i_uid)
i >>= 6;
else if (current->egid == inode->i_gid)
i >>= 3;
if (!(i & 1) &&
!((inode->i_mode & 0111) && suser())) {
retval = -ENOEXEC;
goto exec_error2;
}
if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {
retval = -EACCES;
goto exec_error2;
}
ex = *((struct exec *) bh->b_data); /* read exec-header */
if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
/*
* This section does the #! interpretation.
* Sorta complicated, but hopefully it will work. -TYT
*/
char buf[1023], *cp, *interp, *i_name, *i_arg;
unsigned long old_fs;
strncpy(buf, bh->b_data+2, 1022);
brelse(bh);
iput(inode);
buf[1022] = '\0';
if (cp = strchr(buf, '\n')) {
*cp = '\0';
for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
}
if (!cp || *cp == '\0') {
retval = -ENOEXEC; /* No interpreter name found */
goto exec_error1;
}
interp = i_name = cp;
i_arg = 0;
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
if (*cp == '/')
i_name = cp+1;
}
if (*cp) {
*cp++ = '\0';
i_arg = cp;
}
/*
* OK, we've parsed out the interpreter name and
* (optional) argument.
*/
if (sh_bang++ == 0) {
p = copy_strings(envc, envp, page, p, 0);
p = copy_strings(--argc, argv+1, page, p, 0);
}
/*
* Splice in (1) the interpreter's name for argv[0]
* (2) (optional) argument to interpreter
* (3) filename of shell script
*
* This is done in reverse order, because of how the
* user environment and arguments are stored.
*/
p = copy_strings(1, &filename, page, p, 1);
argc++;
if (i_arg) {
p = copy_strings(1, &i_arg, page, p, 2);
argc++;
}
p = copy_strings(1, &i_name, page, p, 2);
argc++;
if (!p) {
retval = -ENOMEM;
goto exec_error1;
}
/*
* OK, now restart the process with the interpreter's inode.
*/
old_fs = get_fs();
set_fs(get_ds());
if (!(inode=namei(interp))) {
/* get executables inode */
set_fs(old_fs);
retval = -ENOENT;
goto exec_error1;
}
set_fs(old_fs);
goto restart_interp;
}
brelse(bh);
if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||
inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {
retval = -ENOEXEC;
goto exec_error2;
}
if (N_TXTOFF(ex) != BLOCK_SIZE) {
printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
retval = -ENOEXEC;
goto exec_error2;
}
if (!sh_bang) {
p = copy_strings(envc,envp,page,p,0);
p = copy_strings(argc,argv,page,p,0);
if (!p) {
retval = -ENOMEM;
goto exec_error2;
}
}
/* OK, This is the point of no return */
if (current->executable)
iput(current->executable);
current->executable = inode;
for (i=0 ; i<32 ; i++)
current->sigaction[i].sa_handler = NULL;
for (i=0 ; i<NR_OPEN ; i++)
if ((current->close_on_exec>>i)&1)
sys_close(i);
current->close_on_exec = 0;
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
if (last_task_used_math == current)
last_task_used_math = NULL;
current->used_math = 0;
p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
p = (unsigned long) create_tables((char *)p,argc,envc);
current->brk = ex.a_bss +
(current->end_data = ex.a_data +
(current->end_code = ex.a_text));
current->start_stack = p & 0xfffff000;
current->euid = e_uid;
current->egid = e_gid;
i = ex.a_text+ex.a_data;
while (i&0xfff)
put_fs_byte(0,(char *) (i++));
eip[0] = ex.a_entry; /* eip, magic happens :-) */
eip[3] = p; /* stack pointer */
return 0;
exec_error2:
iput(inode);
exec_error1:
for (i=0 ; i<MAX_ARG_PAGES ; i++)
free_page(page[i]);
return(retval);
}
产生缺页中断并由操作系统响应。缺页中断信号产生后,page_fault这个服务程序将对此进行响应,并最终在_page_falut中通过调用call _do_no_page调用到缺页中断处理程序_do_no_page()中执行。
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
address &= 0xfffff000;
tmp = address - current->start_code;
if (!current->executable || tmp >= current->end_data) {
get_empty_page(address);
return;
}
if (share_page(tmp))
return;
if (!(page = get_free_page()))
oom();
/* remember that 1 block is used for header */
block = 1 + tmp/BLOCK_SIZE;
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
bread_page(page,current->executable->i_dev,nr);
i = tmp + 4096 - current->end_data;
tmp = page + 4096;
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
if (put_page(page,address))
return;
free_page(page);
oom();
}
进程调用Exit()函数进行退出,最终会映射到sys_exit()函数去执行,并调用do_exit()函数来处理进程退出的相关事务。进程退出包括两个方面:
int do_exit(long code)
{
int i;
free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
for (i=0 ; i<NR_TASKS ; i++)
if (task[i] && task[i]->father == current->pid) {
task[i]->father = 1;
if (task[i]->state == TASK_ZOMBIE)
/* assumption task[1] is always init */
(void) send_sig(SIGCHLD, task[1], 1);
}
for (i=0 ; i<NR_OPEN ; i++)
if (current->filp[i])
sys_close(i);
iput(current->pwd);
current->pwd=NULL;
iput(current->root);
current->root=NULL;
iput(current->executable);
current->executable=NULL;
if (current->leader && current->tty >= 0)
tty_table[current->tty].pgrp = 0;
if (last_task_used_math == current)
last_task_used_math = NULL;
if (current->leader)
kill_session();
current->state = TASK_ZOMBIE;
current->exit_code = code;
tell_father(current->father);
schedule();
return (-1); /* just to suppress warnings */
}