一、 以fork和execve系统调用为例分析中断上下文的切换
1. fork系统调用过程
fork()函数又叫计算机程序设计中的分叉函数,fork是一个很有意思的函数,它可以建立一个新进程,把当前的进程分为父进程和子进程,新进程称为子进程,而原进程称为父进程。fork调用一次,返回两次,这两个返回分别带回它们各自的返回值,其中在父进程中的返回值是子进程的PID,而子进程中的返回值则返回 0。其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.因此,可以通过返回值来判定该进程是父进程还是子进程。还有一个很奇妙的是:fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。
新创建的子进程几乎但是不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同(但是独立)的一份拷贝,包括文本,数据和bss段、堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝。这就是意味着当父进程调用fork时候,子进程还可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大区别在于他们有着不同的PID。
上面对我们对fork的执行过程进行解释一下,来看它究竟做了哪些操作。 当你调用fork函数时,linux底层究竟怎样进行怎样的操作?为此,我查看linux内核源码来理解,代码位置:init/main.c
static inline _syscall0(int,fork)
内核通过内联操作,在调用fork函数时,实际上是执行到unistd.h中的宏函数syscall0中去。对应代码:
#define __NR_setup 0 /* used only by init, to get system going */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
………
#define _syscall0(type,name) type name(void) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_##name)); if (__res >= 0) return (type) __res; errno = -__res; return -1; }
首先进入_syscall0后,先执行:”0”(__NR_fork)是将fork在sys_call_table[]中对应的函数编号__NR_fork(也就是2)赋值给eax,(在sys_call_table[]中编号2即对应sys_fork函数)。然后执行int $0x80软中断,在set_system_gate(0x80,&system_call);(/linux/kernel/Sched.c中的sched_init函数里)中定义了中断0x80对应着system_call函数(再由iret翻转回到进程的3特权级),所以就会跳转到_system_call中继续执行。
fork系统调用执行流程:
sys_fork里面会做的事:
(1) find_empty_process():在task[64]中为进程申请一个空闲位置并获取进程号
(2) copy_process():
- 为新进程创建task_struct,将原先进程的task_struct的内容复制给新进程
- 给新进程分配页表,并复制原先进程的页表到新进程
- 共享原先进程的文件
- 设置新进程的GDT项
- 将新进程设置成就绪态,参与进程间的轮转
0x80中断
系统调用是要在0特权级下的完成的,也是为了其安全性,那么3特权级翻转到0特权级时发生了什么?
代码位置:\linux\include\asm\System.h
#define set_system_gate(n,addr) _set_gate(&idt[n],15,3,addr)
……
#define _set_gate(gate_addr,type,dpl,addr) __asm__ ("movw %%dx,%%ax\n\t" "movw %0,%%dx\n\t" "movl %%eax,%1\n\t" \ //这里在拼接IDT表
"movl %%edx,%2" : : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), "o" (*((char *) (gate_addr))), "o" (*(4+(char *) (gate_addr))), "d" ((char *) (addr)),"a" (0x00080000))
dpl为3表示系统调用可以由3特权级调用,所以set_system_gate可以在用户态调用,中断使CPU硬件自动将SS、ESP、EFLAGS、CS、EIP寄存器的数值压入内核栈,这里发生了内核态和用户态堆栈的切换,这两个操作都在system_call的一开始进行了,从system_call开始就是在内核态了。