资源描述
linux源代码分析实验报告格式
操作系统报告—fork、exec、wait代码的分析
Linux的fork、exec、wait代码的分析
指导老师:景建笃
组员:王步月
张少恒
完成日期:2005-12-16
一、 设计目的
1.通过对Linux的fork、exec、wait代码的分析,了解一个操作系统进程的创建、执行、等待、退出的过程,锻炼学生分析大型软件代码的能力;
2.通过与同组同学的合作,锻炼学生的合作能力。
二、准备知识
由于我们选的是题目二,所以为了明确分工,我们必须明白进程的定义。经过查阅资料,我们得知进程必须具备以下四个要素:
1、有一段程序供其执行。这段程序不一定是进程专有,可以与其他进程共用。
2、有起码的“私有财产”,这就是进程专用的系统堆栈空间
3、有“户口”,这就是在内核中有一个task_struct结构,操作系统称为“进程控制块”。有了这个结构,进程才能成为内核调度的一个基本单位。同时,这个结构又是进程的“财产登记卡”,记录着进程所占用的各项资源。
4、有独立的存储空间,意味着拥有专有的用户空间:进一步,还意味着除前述的系统空间堆栈外,还有其专用的用户空间堆栈。系统为每个进程分配了一个task_struct结构,实际分配了两个连续的物理页面(共8192字节),其图如下:
对这些基本的知识有了初步了解之后,我们按老师的建议,商量分工。如下:
四、 小组成员以及任务分配
1、王步月:分析进程的创建函数fork.c,其中包含了get_pid和do_fork get_pid,写出代码分析结果,并画出流程图来表示相关函数之间的相互调用关系。所占工作比例35%。
2、张少恒:分析进程的执行函数exec.c,其中包含了do_execve。写出代码分析结果,并画出流程图来表示相关函数之间的相互调用关系。所占工作比例35% 。
3、余波:分析进程的退出函数exit.c,其中包含了do_exit、sys_wait4。写出代码分析结果,并画出流程图来表示相关函数之间的相互调用关系。所占工作比例30% 。
五、各模块分析:
1、fork.c
一)、概述
进程大多数是由FORK系统调用创建的.fork能满足非常高效的生灭机制.除了0进程等少数一,两个进程外,几乎所有的进程都是被另一个进程执行fork系统调用创建的.调用fork的进程是父进程,由fork创建的程是子进程.每个进程都有一个父进程.而一个进程可以有多个子进程.父进程创建一个子进程完成一定的工作时,往往希望子进程结束后,还要把控制权交给父进程,因此子进程不应把父进程覆盖掉.fork系统调用创建子进程的做法,是把自己复制给子进程,也就是说,新创建的子进程是父进程的一个副本.继而子进程通过exec系统调用,用一个新的程序来覆盖子进程的内存空间,从而执行那个新程序.系统调用exit可以终止一个进程的执行,子进程也常常用exit系统调用来自我终止.子进程终止之后,进入僵死(zombie)状态,父进程可通过执行wait系统调用来实现与子进程的终止同步,接受子进程的返回状态和返回参数.
二)、代码分析
int do_fork(unsigned long clone_flags, unsigned long stack_start,
struct pt_regs *regs, unsigned long stack_size)
{
int retval;
unsigned long flags;
struct task_struct *p;
struct completion vfork;
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return -EINVAL;
retval = -EPERM;/* 将retval赋值-ENOMEM,作为task_struct结构申请失败时的返回值*/
if (clone_flags & CLONE_PID) {/* 若clone_flags的位是置位的*/
/* 若调用do_fork的当前(父)进程不是idle进程(其pid=0)*/
if (current->pid)
goto fork_out;
}
retval = -ENOMEM;/*返回错误信息*/
p = alloc_task_struct(); /* 申请一个新的task_struct结构*/
if (!p)
goto fork_out;
*p = *current;/* 将当前(父)进程task_struct结构值赋给新创建的(子)进程*/
p->tux_info = NULL;
p->cpus_allowed_mask &= p->cpus_allowed;
retval = -EAGAIN;
/* 若子(新)进程所属的用户拥有的进程数已达到规定的限制值,
* 则跳转至bad_fork_fre */?
if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur
&& !capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE))
goto bad_fork_free;
/* user->__count增一,user->processes(用户拥有的进程数)增一 */
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
/* 若系统进程数超过最大进程数则跳转至bad_fork_cleanup_count */
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
get_exec_domain(p->exec_domain);/* 若正在执行的代码是符合iBCS2标准的程序,则增加相对应模块的引用数目 */
/* 若正在执行的代码属于全局执行文件结构格式则增加相对应模块的引用数目 */
if (p->binfmt && p->binfmt->module)
__MOD_INC_USE_COUNT(p->binfmt->module);
p->did_exec = 0;/* 将子进程标志为尚未执行 */
p->swappable = 0; /* 清标志,使内存页面不可换出 */
p->state = TASK_UNINTERRUPTIBLE;/* 将子进程的状态置为uninterruptible */
copy_flags(clone_flags, p);/* 将clone_flags略加修改写入p->flags */
p->pid = get_pid(clone_flags);/* 调用kernel/fork.c:get_pid()为子进程分配一个 pid. 若是clone系统调用且
* clone_flags中CLONE_PID位为1,那么父子进程共享一个pid号;否则要分配给子进
* 程一个从未用过的pid */
if (p->pid == 0 && current->pid != 0)
goto bad_fork_cleanup;
/* 对运行队列接口初始化 */
INIT_LIST_HEAD(&p->run_list);
p->p_cptr = NULL;
init_waitqueue_head(&p->wait_chldexit);/* 初始化wait_chldexit等待队列wait_chldexit用于在进程结束时,或发出
* 系统调用wait4后,为了等待子进程结束,而将自己(父进程)睡眠在该队列上 */
p->vfork_done = NULL;
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}
spin_lock_init(&p->alloc_lock);
p->sigpending = 0;
init_sigpending(&p->pending);
p->it_real_value = p->it_virt_value = p->it_prof_value = 0;
p->it_real_incr = p->it_virt_incr = p->it_prof_incr = 0;
init_timer(&p->real_timer);
p->real_timer.data = (unsigned long) p;
p->leader = 0; /* session leadership doesn't inherit */
p->tty_old_pgrp = 0;
p->times.tms_utime = p->times.tms_stime = 0;
p->times.tms_cutime = p->times.tms_cstime = 0;
#ifdef CONFIG_SMP
{
int i;
/* ?? should we just memset this ?? */
for(i = 0; i < smp_num_cpus; i++)
p->per_cpu_utime[cpu_logical_map(i)] =
p->per_cpu_stime[cpu_logical_map(i)] = 0;
spin_lock_init(&p->sigmask_lock);
}
#endif
p->array = NULL;
p->lock_depth = -1; /* -1 = 没有锁 */
p->start_time = jiffies_64;/* 将当前的jiffies值作为子进程的创建时间*/
/* task_struct结构初始化完毕 */
retval = -ENOMEM;
/* copy all the process information */
if (copy_files(clone_flags, p))/* 复制所有的进程信息,根据clone_flags复制或共享父进程的打开文件表*/
goto bad_fork_cleanup;
if (copy_fs(clone_flags, p))/* 根据clone_flags复制或共享父进程的系统信息 */
goto bad_fork_cleanup_files;
if (copy_sighand(clone_flags, p))/* 根据clone_flags复制或共享父进程的信号处理句柄 */
goto bad_fork_cleanup_fs;
if (copy_mm(clone_flags, p))/* 根据clone_flags复制或共享父进程的存储管理信息 */
goto bad_fork_cleanup_sighand;
if (copy_namespace(clone_flags, p))/* 为子进程复制父进程系统空间堆栈 */
goto bad_fork_cleanup_mm;/* 若系统空间堆栈复制失败跳转至bad_fork_cleanup_mm */
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_namespace;
p->semundo = NULL;
/* 将子进程task_struct结构的self_exec_id赋给parent_exec_id */
p->parent_exec_id = p->self_exec_id;
p->swappable = 1;/* 新进程已经完成初始化,可以换出内存,所以将p->swappable赋1 */
p->exit_signal = clone_flags & CSIGNAL;/* 设置系统强行退出时发出的信号 */
p->pdeath_signal = 0;/* 设置p->pdeath_signal */
/* * Share the timeslice between parent and child, thus the
* total amount of pending timeslices in the system doesnt change,
* resulting in more scheduling fairness.*/
__save_flags(flags);
__cli();
if (!current->time_slice)/* 将父进程的时间片减半 */
BUG();
p->time_slice = (current->time_slice + 1) >> 1;
p->first_time_slice = 1;
current->time_slice >>= 1;
p->sleep_timestamp = jiffies;
if (!current->time_slice) {
current->time_slice = 1;
scheduler_tick(0,0);
}
__restore_flags(flags);
retval = p->pid;/* 如果一切顺利,将子进程的pid作为返回值 */
p->tgid = retval;
INIT_LIST_HEAD(&p->thread_group);
/* Need tasklist lock for parent etc handling! */
write_lock_irq(&tasklist_lock);/* 给进程队列加锁 */
/* CLONE_PARENT re-uses the old parent */
p->p_opptr = current->p_opptr;
p->p_pptr = current->p_pptr;
if (!(clone_flags & CLONE_PARENT)) {
p->p_opptr = current;
if (!(p->ptrace & PT_PTRACED))
p->p_pptr = current;
}
if (clone_flags & CLONE_THREAD) {
p->tgid = current->tgid;
list_add(&p->thread_group, ¤t->thread_group);
}
SET_LINKS(p);/* 将子进程的task_struct结构链入进程队列 */
hash_pid(p);/* 将子进程的task_struct结构链入进程hash表 */
nr_threads++;/* 系统进程计数递增一 */
write_unlock_irq(&tasklist_lock);/* 解除对进程队列的封锁 */
if (p->ptrace & PT_PTRACED)
send_sig(SIGSTOP, p, 1);
wake_up_forked_process(p); /* 最后做这件事,唤醒子进程 */
++total_forks;/* total_forks增一*/
if (clone_flags & CLONE_VFORK)
wait_for_completion(&vfork);
else
current->need_resched = 1;
fork_out:/* 若是vfork()调用do_fork,发down信号*/
return retval;/* 退出do_fork(),返回retval值*/
bad_fork_cleanup_namespace:
exit_namespace(p);
bad_fork_cleanup_mm:
exit_mm(p);
bad_fork_cleanup_sighand:/* 处理子进程task_struct结构与信号处理相关的数据成员, 并删除信号队列中与子进程相
* 关的信号量 */
exit_sighand(p);
bad_fork_cleanup_fs:/* 处理子进程task_struct结构与文件系统信息相关的数据成员 */
exit_fs(p); /* blocking */
bad_fork_cleanup_files:/* 处理子进程task_struct结构与打开文件表相关的数据成员, 并释放子进程的files_struct
* 结构 */
exit_files(p); /* blocking */
bad_fork_cleanup:/* 若正在执行的代码是符合iBCS2标准的程序,则减少相对应模块的引用数目 */
put_exec_domain(p->exec_domain);
if (p->binfmt && p->binfmt->module)
__MOD_DEC_USE_COUNT(p->binfmt->module);
bad_fork_cleanup_count:/* 若正在执行的代码属于全局执行文件结构格式则减少相对应模块的引用数目 */
atomic_dec(&p->user->processes);
free_uid(p->user);/* 清除子进程在user队列中的信息 */
bad_fork_free:
free_task_struct(p);/* 释放子进程的task_struct结构 */
goto fork_out;
}
三)、程序框图如下:
2、exec.c
一)、概述
进程通常是由父进程复制出来的(由fork()或clone())。假若子进程只是父进程的“影子”,那么没有什么意义了。所以,执行一个新的可执行程序才是创建进程的意义所在。在Linux中,提供了一个系统调用execve(),其内核入口是sys_execve()。
二)、代码分析
asmlinkage int sys_execve(struct pt_regs regs )
{
int error;
char *filename;
filename=getname((char*)regs.ebx);
error=PTR_ERR(filename);
if(IS_ERR(filename))
goto out;
error=do_execve(filename,(char**)regs.ecx,(char** )regs.edx,®s);
if(error==0)
current->ptrace &= ~PT_DTRACE;
putname(filename);
out:
return error;
}
regs.ebx中的内容为应用程序中调用相应库函数时的第一个参数。getname()把regs.ebx所指向的字符串从用户空间拷贝到系统空间,在系统空间建立起一个副本。getname()通过dogetname()从用户空间拷贝字符串。
建立起一个可执行文件路径名的副本后,sys_execve()调用do_execve()以完成其主体部分的工作。
int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)
{
struct linux_binprm bprm;
//用于组织运行可执行文件所需的信息,通过此变量与负责处理其部分工作的其他//函数通信;在do_execve返回时废弃;
struct file *file;
int retval;
int i;
file = open_exec(filename); //找到给定的可执行文件并打开;
retval = PTR_ERR(file);
if (IS_ERR(file))
return retval; //检测打开文件是否有错;
首先将给定路径名的可执行文件找到并打开,因而调用open_exec()来实现。函数返回一个file结构指针,代表读入可执行文件的上下文,保存在变量bprm中。代码开始定义了一个linux_binprm结构的变量bprm,用于将运行一个可执行文件所需的信息组织在一起。linux_binprm结构定义(在include/linux/binfmts.h中)如下:
struct linux_binprm{
char buf[BINPRM_BUF_SIZE];
struct page *page[MAX_ARG_PAGES];
unsigned long p; /* current top of mem */
int sh_bang;
struct file * file;
int e_uid, e_gid;
kernel_cap_t cap_inheritable, cap_permitted, cap_effective;
int argc, envc;
char * filename; /* Name of binary */
unsigned long loader, exec;
};
在do_execve()中,接下来的代码是:
bprm.p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);//初始化bprm结构的128k页表除去第一个argv[0]);
memset(bprm.page, 0, MAX_ARG_PAGES*sizeof(bprm.page[0]));
//将参数页面指针数组初始化为零;
bprm.file = file; //可执行文件的上下文;
bprm.filename = filename; //可执行文件的路径名;
bprm.sh_bang = 0; //可执行文件的性质;
bprm.loader = 0;
bprm.exec = 0;
if ((bprm.argc = count(argv, bprm.p / sizeof(void *))) < 0) {
allow_write_access(file);
fput(file);
return bprm.argc;
} //根据argv数组计算非空指针个数并赋给argc成员;
if ((bprm.envc = count(envp, bprm.p / sizeof(void *))) < 0) {
allow_write_access(file);
fput(file);
return bprm.envc;
} //统计环境变量个数并且赋值给envc的各个成员;
retval = prepare_binprm(&bprm); ////进行访问权限等内容的安全检测后,读入可执行文件前128字节;
if (retval < 0)
goto out;
变量bprm.sh_bang表示可执行文件的性质,此时初始化为0。其他两个变量也设置为0,因为现在还不知道文件性质。Bprm中定义了一个参数页面指针数组,通过memset()将此数组全设置为0;将bprm.p设置为这些页面的总和减去一个指针的大小,原因是argv[0]是可执行文件的路径名。函数count()对用户空间作为参数传过来的字符串指针数组argv[]和环境变量envp[]进行计数。
完成计数后,do_execve()调用prepare_binprm()对bprm中的其他成员准备信息。可执行文件的开头128个字节包含了文件属性的一些重要信息,并将这128个信息读入到bprm的缓冲区中。
retval = copy_strings_kernel(1, &bprm.filename, &bprm); //从系统空间中拷贝可执行文件路径名;
if (retval < 0)
goto out;
bprm.exec = bprm.p;
retval = copy_strings(bprm.envc, envp, &bprm); //从用户空间拷贝环境信息;
if (retval < 0)
goto out;
retval = copy_strings(bprm.argc, argv, &bprm); //从用户空间拷贝参数信息;
if (retval < 0)
goto out;
由于可执行文件的路径名已经在系统空间中了,所以调用copy_strings_kernel()拷贝到bprm中;其他的argv[]和envp[]还存在于用户空间,调用copy_strings()拷贝到bprm中。
至此,可执行文件运行所需的所有信息都已组织到变量bprm中了,接下来的代码就是装入并运行了。
retval = search_binary_handler(&bprm,regs); //从formats中搜索能够识别的二进制处理程序并将其装入内核,以便执行;
if (retval >= 0) //找到能够识别的二进制处理程序;
/* execve success */
return retval;
out:
/* Something went wrong, return the inode and free the argument pages*/
allow_write_access(bprm.file);
if (bprm.file)
fput(bprm.file);
for (i = 0 ; i < MAX_ARG_PAGES ; i++) {
struct page * page = bprm.page[i];
if (page)
__free_page(page); //释放为参数和环境所分配的物理页面,并返回一个负数通知调用者调用失败;
}
return retval;
}
在此段代码中,最重要的的是函数search_binary_handler(),其中有两个for()循环。内层循环是每个formats队列(每个成员是认识并处理唯一一种文件格式的二进制处理程序)成员的循环,让每个成员调用自己的load_binary()去识别;若能识别则装入可执行文件并运行,返回一个正数或零;不能则返回一个负数。内层寻常结束后,如果返回的负数是
-ENOEXEC,表示所有的formats成员都不能识别此文件格式。如果内核支持动态安装模块,就根据目标文件的第2、3个字节生成一个binfmt模块名,通过request_module()试着将模块装入,以便外层for()循环在装入模块后再来一次内层循环。
三)、程序框图如下:
3、exit.c
一)、概述
Exit.c中核心是do-exit(),cpu不会从do-exit()返回,中断服务程序不调用do-exit,in-interrupt()对此加以检查.因此在分析程序时,先浏览整段代码,然后开始分析do-exit()函数.在这个主函数中涉及到几个函数的调用,分别对这些函数进行分析,分析每个函数的功能和作用.
进程在结束退出前要释放所有系统资源.比如存储空间,已打开文件,工作目录,信号处理表等是通过函数_exit_mm(), _exit_files(),_exit_fs(),_exit_sighand()来完成的. _exit_mm()存储空间的释放,调用mm_release()唤醒睡眠中的父进程,当前进程状态改成TASK_ZOMBIE,调用exit-notify(),通知父进程子进程已退出.cpu完成exit-notify()后,回到do-exit(),调用schedule().
Sys_wait4()函数中,参数pid为某一子进程的进程号,此函数的作用是等待子进程变成TASK_ZOMBIE.task-struct结构一开始就在当前进程的系统堆栈分配一个等待队列. 当task-struct结构释放后,子进程就结束.在这个过程中,父进程正在wait4中等待.unhash_process()把子进程删除,把子进程的其他统计信息也合并入父进程.然后就调用task_release()释放子进程残存的资源.
二)、代码分析
NORET_TYPE void do_exit(long code)//将退出代码作为参数处理,在其类型前用到特殊符号NORET_TYPE
{
struct task_struct *tsk = current;
if (in_interrupt())
panic("Aiee, killing interrupt handler!");
if (!tsk->pid)
panic("Attempted to kill the idle task!");
if (tsk->pid == 1)
panic("Attempted to kill init!");
tsk->flags |= PF_EXITING;
del_timer_sync(&tsk->real_timer);
fake_volatile:
#ifdef CONFIG_BSD_PROCESS_ACCT
acct_process(code);
#endif
if (current->tux_info) {
#ifdef CONFIG_TUX_DEBUG
printk("Possibly unexpected TUX-thread exit(%ld) at %p?\n",
code, __builtin_return_address(0));
#endif
current->tux_exit();
}
__exit_mm(tsk);//释放分配给它的内存
lock_kernel();
sem_exit();//释放信号量和其他system VIPC结构
__exit_files(tsk);//释放分配给它的文件
__exit_fs(tsk);//释放文件系统的数据
exit_namespace(tsk);
exit_sighand(tsk);//释放信号量处理程序表
exit_thread();
if (current->leader)
disassociate_ctty(1);
put_exec_domain(tsk->exec_domain);
if (tsk->binfmt && tsk->binfmt->module)
__MOD_DEC_USE_COUNT(tsk->binfmt->module);
tsk->exit_code = code;
exit_notify();//调用exit_notify,它会警告当前退出任务的祖先进程和其进程组中的所有成员该进程正在退出
schedule();/*调用schedule(),释放CPU,这对schedule()
的调用从来不返回因为它跳转到下一个进程的上下文,
不会再跳转回来,这是现在正在退出的进程最后一次
拥有CPU的机会*/
BUG();
asmlinkage long sys_exit(int error_code)
{
do_exit((error_code&0xff)<<8);
}
asmlinkage long sys_wait4(pid_t pid,unsigned int * stat_addr, int options, struct rusage * ru)/*sys_wait4有很多参数。pid是目标进程的PID.O和负值是特殊情况 如果stat_addr非空,它就是所得子孙进程的退出状态应该拷贝到的地址
options是一些可能定义sys-wait4的操作的标志的集合。如果ru非空,
那么它就是所得子孙进程资源使用信息应该拷贝到的地址*/
{
int flag, retval;
DECLARE_WAITQUEUE(wait, current);
struct task_struct *tsk;
if (options & ~(WNOHANG|WUNTRACED|__WNOTHREAD|__WCLONE|__WALL))//如果提供了无效选项sys_wait4就返回错误代码
return -EINVAL;
add_wait_queue(¤t->wait_chldexit,&wait);//增加到等待队列中
repeat:
flag = 0;
current->state = TASK_INTERRUPTIBLE;
read_lock(&tasklist_lock);
tsk = current;
do {
struct task_struct *p;
for (p = tsk->p_cptr ; p ; p = p->p_osptr)//循环遍历该进程的直接子进程
{
if (pid>0) {//根据pid参数的值筛选出不匹配的PID
if (p->pid != pid)
continue;
} else if (!pid) {
if (p->pgrp != current->pgrp)
continue;
} else if (pid != -1) {
if (p->pgrp != -pid)
continue;
}
if (((p->exit_signal != SIGCHLD) ^ ((options & __WCLONE) != 0))
&& !(options & __WALL))
cont
展开阅读全文