作业啦,仔细看了下fork的代码,大致记录一下。。
fork分析
不像Windows要执行一个程序直接使用CreateProcess
,在Linux下先使用fork
创建子进程,这个子进程像细胞分裂一样,与父进程拥有一样的代码段与数据段等,接着再调用execve
替换子进程的内容,使它变成目标程序再执行,也就是说Linux下先用fork
分配程序运行所需要的资源,再用execve
载入目标程序,本次主要分析fork
的代码。
相关文件
- arch/i386/kernel/process.c:提供系统调用接口,对
do_fork
进行简单封装 - include/linux/sched.h:定义了
struct task_struct
结构,即进程控制块结构 - kernel/fork.c:
do_fork
的具体实现 - include/asm-i386/ptrace.h:定义了
struct pt_regs
,存放了系统调用时用户空间传来的寄存器值
总述
fork会按照分配内核栈空间->设置task_struct->拷贝共享资源->设置内核栈
的顺序复制父进程的资源:
分配内核栈,此结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20(THREAD_SIZE+(unsinged
long)P) +------> +---------------+ <--------+
| pt_regs | |
((struct pt_regs*)(THRE | | |
AD_SIZE)+(unsigned +-----> +---------------+ |
long)P) - 1) | | |
| | |
| | |
| | THREAD_SIZE
| | |
+---------------+ |
| | |
| | |
| | |
| | |
| | |
| | |
+---------------+ |
| task_struct | |
+---------------+ <--------+接着设置内核栈的
tast_struct
结构,它在include/linux/sched.h
中定义,它里面记录着进程执行所需的各种重要信息,他们大致分为:性质,状态:运行状态,用户组,权限等
资源:内存,文件等
组织:进程树情况task_struct
中大部分内容是指针,包括各种资源指针,于是需要根据需求,要么增加针指所指目标的引用计数,要么对目标进行指定深度的复制,使指针指向新的备份。当将
task_struct
的内容处理完毕,一个进程要运行就只差内核栈内容了,例如用户空间的各种上下文信息都保存在pt_regs中,这里将内核栈复制后,通过设置不同的返回值,子进程就可以与父进程返回,执行相应内容了。
细节分析
从
arch/i386/kernel/process.c
可见与创建子进程有关的系统调用有三个,它们只是对do_fork
进行简单包装,本篇以sys_fork
为主线分析do_fork
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0);
}
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx; //第二个参数为新栈帧的地址
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s, 0);
}
asmlinkage int sys_vfork(struct pt_regs regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0);
}do_fork
的声明如下,第一个参数为复制的标志,如上所见,在创建子进程时有多种不同的方式,他们的区别即复制父进程资源时以何种深度进行,是共享资源还是创建独立的备份。第二个参数代表子进程的用户栈起始位置,在sys_fork
方式中和父进程一样,都来自regs
中。第三个参数是系统调用时传入的寄存器值,第四个参数是栈大小。1
2int do_fork(unsigned long clone_flags, unsigned long stack_start,
struct pt_regs *regs, unsigned long stack_size);分配一个内核栈,并将当前进程(父进程)的
task_struct
整体赋值给子进程,接下来只需根据需要调整它里面的值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int retval = -ENOMEM;
struct task_struct *p;
DECLARE_MUTEX_LOCKED(sem); //这个信号量初始为0,即可用来使自己阻塞
if (clone_flags & CLONE_PID) { //让父子进程拥有同样的进程号
if (current->pid) //只有0号进程有权限
return -EPERM;
}
current->vfork_sem = &sem;
p = alloc_task_struct(); //为子进程创建task_struct结构
if (!p)
goto fork_out;
*p = *current; //复制父进程的task_struct现在根据实际来更改从父进程那里复制过来的
task_struct
的值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55retval = -EAGAIN;
if (atomic_read(&p->user->processes) >= p->rlim[RLIMIT_NPROC].rlim_cur) //用户拥有的进程数是否超过限制
goto bad_fork_free;
atomic_inc(&p->user->__count); //添加user_struct的引用计数
atomic_inc(&p->user->processes); //user_struct拥有的进程数加一
/*
* Counter increases are protected by
* the kernel lock so nr_threads can't
* increase under us (but it may decrease).
*/
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
get_exec_domain(p->exec_domain); //获取进程执行域
if (p->binfmt && p->binfmt->module) //获取到并且它有对应模块
__MOD_INC_USE_COUNT(p->binfmt->module); //对应驱动模块引用计数加一
p->did_exec = 0;
p->swappable = 0;
p->state = TASK_UNINTERRUPTIBLE; //将进程状态设置为不可被软中断唤醒,在最后用wake_up唤醒
copy_flags(clone_flags, p); //对传入的clone_flags进行解析后赋值给p的flags位
p->pid = get_pid(clone_flags); //分配一个可用pid,在其内部:
//若设置了与父进程共享PID则直接返回,前面已有检查只有0号进程能这样做
p->run_list.next = NULL; //将运行队列设置为空
p->run_list.prev = NULL;
if ((clone_flags & CLONE_VFORK) || !(clone_flags & CLONE_PARENT)) {
p->p_opptr = current;
if (!(p->ptrace & PT_PTRACED))
p->p_pptr = current;
}
p->p_cptr = NULL;
init_waitqueue_head(&p->wait_chldexit); //将等待子进程退出队列置为空
p->vfork_sem = NULL;
spin_lock_init(&p->alloc_lock);
p->sigpending = 0; //收到待处理的信号数为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;
p->lock_depth = -1; /* -1 = no lock */
p->start_time = jiffies; //进程创建时间现在复制资源,以下的复制根据传入的参数
clone_flags
进行:1
2
3
4
5
6
7
8
9
10
11
12retval = -ENOMEM;
/* copy all the process information */
if (copy_files(clone_flags, p)) //复制打开的资源,若CLONE_FILES被置位,则只增加文件结构引用计数
goto bad_fork_cleanup;
if (copy_fs(clone_flags, p)) //复制文件系统,只有CLONE_FS未被置位,才复制fs_struct结构
goto bad_fork_cleanup_files;
if (copy_sighand(clone_flags, p)) //复制信号处理例程
goto bad_fork_cleanup_fs;
if (copy_mm(clone_flags, p)) //复制内存内容,即使CLONE_VM|CLONE_VFORK为0,也只会复制mm_struct,vm_area_struct区域,而不会
//复制内存的内容,对于只读数据当然可以共享,对于可写且不共享的数据,采用写时复制,即更改对应页权限
//为只读,当写时会触发异常,在异常处理例程中再分配新的物理页面,复制数据,并改页表项
goto bad_fork_cleanup_sighand;接着设置内核栈:
1
2
3
4retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); //复制内核栈
if (retval)
goto bad_fork_cleanup_sighand;
p->semundo = NULL;copy_thread
设置了子程序的返回值,内核栈指针,返回地址,pt_regs等信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
asm volatile("movl %%" #seg ",%0":"=m" (*(int *)&(value))) ;将寄存器内容存入指定内存
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1; //得到栈底处->childregs
struct_cpy(childregs, regs); //将系统调用的寄存器赋值
childregs->eax = 0; //子进程返回值为0
childregs->esp = esp; //新的子进程用户栈位置,fork时和父进程一样
p->thread.esp = (unsigned long) childregs; //pt_regs结构位置
p->thread.esp0 = (unsigned long) (childregs+1); //内核栈底
p->thread.eip = (unsigned long) ret_from_fork; //返回地址
savesegment(fs,p->thread.fs); //将fs,gs存入thread
savesegment(gs,p->thread.gs);
unlazy_fpu(current);
struct_cpy(&p->thread.i387, ¤t->thread.i387);
return 0;
}现在准备工作基本做完,再设置几个状态,就可以将进程加入等待队列了,这样子进程就可以被调度了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35p->parent_exec_id = p->self_exec_id; //父进程的执行域为当前进程的执行域
/* ok, now we should be set up.. */
p->swappable = 1; //子进程可被换出
p->exit_signal = clone_flags & CSIGNAL; //退出时发出的信号
p->pdeath_signal = 0; //父进程退出时不像子进程发信号
p->counter = (current->counter + 1) >> 1; //父子运行时间配额将平分父进程的时间
current->counter >>= 1;
if (!current->counter)
current->need_resched = 1;
retval = p->pid; //返回值,即父进程返回子进程的pid
p->tgid = retval;
INIT_LIST_HEAD(&p->thread_group); //初始化线程组
write_lock_irq(&tasklist_lock);
if (clone_flags & CLONE_THREAD) { //如果创建的是线程,则将子"线程"加入当前进程的线程组
p->tgid = current->tgid;
list_add(&p->thread_group, ¤t->thread_group);
}
SET_LINKS(p); //链如内核进程队列
hash_pid(p); //按pid散列链入散列队列
nr_threads++;
write_unlock_irq(&tasklist_lock);
if (p->ptrace & PT_PTRACED)
send_sig(SIGSTOP, p, 1);
wake_up_process(p); /* do this last */ //唤醒子进程,将其加入可运行等待队列
++total_forks;
fork_out:
if ((clone_flags & CLONE_VFORK) && (retval > 0)) //如果是vfork且pid大于0即子进程创建成功,则让父进程阻塞
down(&sem);
return retval;参考
[0] 毛德操:《Linux内核源码情景分析·上》
[1] kernel.org:Linux 2.4.0