【推荐阅读】
从一个比较有意思的题开始说起,最近要找工作无意间看到一个关于unix/linux中fork()的面试题:
1 #include<sys/types.h>
2 #include<stdio.h>
3 #include<unistd.h>
4 int main(void)
5 {
6 int i;
7 int buf[100]={1,2,3,4,5,6,7,8,9};
8 for(i=0;i<2;i++)
9 {
10 fork();
11 printf("+");
12 //write("/home/pi/code/test_fork/test_fork.txt",buf,8);
13 write(STDOUT_FILENO,"-",1);
14 }
15 return 0;
16
17 }
题目要求是从上面的代码中确定输出的“+”的数量,我后面加了一个“-”,再确定输出“-”的数量。
先给答案:“+”8次,“-”6次
1 ---++--++-++++
上面的这段代码很简单,包含的内容却有很多,有进程产生、系统调用、不带缓冲I/O、标准I/O。
linux中产生一个进程的调用函数过程如下:
fork()---------->sys_fork()-------------->do_fork()---------->copy_process()
fork()、vfork()、_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。do_fork()完成了创建中的大部分工作,该函数调用copy_process()函数,
从用户空间调用fork()函数到执行系统调用产生软件中断陷入内核空间,在内核空间执行do_fork()函数,主要是复制父进程的页表、内核栈等,如果要执行子进程代码还要调用exac()函数拷贝硬盘上的代码到位内存上,由于刚创建的子进程没有申请内存,目前和父进程共用父进程的代码段、数据段等,没有存放子进程自己代码段数据段的内存,此时会产生一个缺页异常,为子进程申请内存,同时定制自己的全局描述GDT、局部描述符LDT、任务状态描述符TSS,下面从代码中分析这个过程然后在回答上面为什么“+”是8次,“-”6次。
调用fork()函数执行到了unistd.h中的宏函数syscall0
/* XXX - _foo needs to be __foo, while __NR_bar could be _NR_bar. */
/*
* Don't remove the .ifnc tests; they are an insurance against
* any hard-to-spot gcc register allocation bugs.
*/
#define _syscall0(type,name) \
type name(void) \
{ \
register long __a __asm__ ("r10"); \
register long __n_ __asm__ ("r9") = (__NR_##name); \
__asm__ __volatile__ (".ifnc %0%1,$r10$r9\n\t" \
".err\n\t" \
".endif\n\t" \
"break 13" \
: "=r" (__a) \
: "r" (__n_)); \
if (__a >= 0) \
return (type) __a; \
errno = -__a; \
return (type) -1; \
}
将宏函数展开后变为
1 /* XXX - _foo needs to be __foo, while __NR_bar could be _NR_bar. */
2 /*
3 * Don't remove the .ifnc tests; they are an insurance against
4 * any hard-to-spot gcc register allocation bugs.
5 */
7 int fork(void)
8 {
9 register long __a __asm__ ("r10"); \
10 register long __n_ __asm__ ("r9") = (__NR_##name); \
11 __asm__ __volatile__ (".ifnc %0%1,$r10$r9\n\t" \
12 ".err\n\t" \
13 ".endif\n\t" \
14 "break 13" \
15 : "=r" (__a) \
16 : "r" (__n_)); \
17 if (__a >= 0) \
18 return (type) __a; \
19 errno = -__a; \
20 return (type) -1; \
21 }
##的意思就是宏中的字符直接替换
如果name = fork,那么在宏中__NR_##name就替换成了__NR_fork了。
__NR_##name是系统调用号,##指的是两次宏展开.即用实际的系统调用名字代替"name",然后再把__NR_...展开.如name == ioctl,则为__NR_ioctl。
上面的汇编目前还是没有怎么弄懂-------
int $0x80 是所有系统调用函数的总入口,fork()是其中之一,“0”(_NR_fork) 意思是将fork在sys_call_table[]中对应的函数编号_NR_fork也就是2,将2传给eax寄存器。这个编号就是sys_fork()函数在sys_call_table中的偏移值,其他的系统调用在sys_call_table均存在偏移值()。
int $0x80 中断返回后,将执行return (type) -1----->展开就是return (int) __a;产生int $0x80软件中断,CPU从3级特权的进程跳到0特权级内核代码中执行。中断使CPU硬件自动将SS、ESP、EFLAGGS、CS、EIP这五个寄存器的值按照这个顺序压人父进程的内核栈,这些压栈的数据将在后续的copy_process()函数中用来初始化进程1的任务状态描述符TSS
CPU自动压栈完成后,跳转到system_call.s中的_system_call处执行,继续将DS、ES、FS、EDX、ECX、EBX压栈(这些压栈仍旧是为了初始化子进程中的任务状态描述符TSS做准备)。最终内核通过刚刚设置的eax的偏移值“2”查询sys_call_table[],知道此次系统调用对应的函数是sys_fork()。跳转到_sys_fork处执行。
注意:一个函数的参数不是由函数定义的,而是由函数定义以外的程序通过压栈的方式“做”出来的,是操作系统底层代码与应用程序代码写作手法的差异之一。我们知道在C语言中函数运行时参数是存在栈中的,根据这个原理操作系统设计者可以将前面程序强行压栈的值作为函数的参数,当调用这个函数时这些值就是函数的参数。
sys_fork函数
asmlinkage int sys_fork(void)
{
#ifndef CONFIG_MMU
/* fork almost works, enough to trick you into looking elsewhere:-( */
return -EINVAL;
#else
return do_fork(SIGCHLD, user_stack(__frame), __frame, 0, NULL, NULL);
#endif
}
do_fork函数
/*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
struct pid *pid = alloc_pid();
long nr;
if (!pid)
return -EAGAIN;
nr = pid->nr;
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
dup_task_struct
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
}
if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
/*
* We'll start up with an immediate SIGSTOP.
*/
sigaddset(&p->pending.signal, SIGSTOP);
set_tsk_thread_flag(p, TIF_SIGPENDING);
}