Linux 0.11启动过程分析(一)
Linux 0.11 fork 函数(二)
Linux0.11 缺页处理(三)
Linux0.11 根文件系统挂载(四)
Linux0.11 文件打开open函数(五)
Linux0.11 execve函数(六)
Linux0.11 80X86知识(七)
Linux0.11 内核体系结构(八)
Linux 系统中创建新进程使用 fork() 系统调用。所有进程都是通过复制进程 0 而得到的,都是进程 0 的子进程。在创建新进程的过程中,系统首先在任务数组中找出一个还没有被任何进程使用的空项( task[NR_TASKS] )。
然后系统为新建进程在主内存区中申请一页内存( 4K 大小)来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。
随后对复制的任务数据结构进行修改。设置初始运行时间片为 15 个系统滴答数( 150ms )。接着根据当前进程设置任务状态段 TSS 中各寄存器的值。新建进程 内核态堆栈指针 tss.esp0 被设置成新进程任务数据结构所在内存页面的 顶端,而 堆栈段 tss.ss0 被设置成 内核数据段选择符 。tss.ldt 被设置为 局部表描述符 在GDT中的索引值。
此后系统设置新任务的代码和数据段基址、限长,并复制当前进程内存分页管理的页表。注意,此时系统并不为新的进程分配实际的物理内存页面,而是让它共享其父进程的内存页面。只有当父进程或新进程中任意一个有写内存操作时,系统才会为执行写操作的进程分配相关的独立使用的内存页面。
随后,如果父进程中有文件是打开的,则应将对应文件的打开次数增加1。接着在GDT中设置新任务的 TSS 和 LDT 描述符项,其中基地址信息指向新进程任务结构中的 tss 和 ldt 。最后再将新任务设置成可运行状态并返回新进程号。
struct tss_struct {
long back_link; /* 指向前一任务的 TSS 选择符 */ /* 16 high bits zero */
long esp0; /* 特权级 0 栈中偏移量指针 */
long ss0; /* 特权级 0 堆栈段选择符 */ /* 16 high bits zero */
long esp1; /* 特权级 1 栈中偏移量指针 */
long ss1; /* 特权级 1 堆栈段选择符 */ /* 16 high bits zero */
long esp2; /* 特权级 2 栈中偏移量指针 */
long ss2; /* 特权级 2 堆栈段选择符 */ /* 16 high bits zero */
long cr3; /* 控制寄存器字段,含有任务使用的页目录物理基地址 */
long eip; /* 指令指针 */
long eflags; /* 标志寄存器 */
long eax,ecx,edx,ebx; /* 通用寄存器字段 */
long esp; /* 栈中偏移量指针 */
long ebp; /* 堆栈的帧指针 */
long esi; /* 源变址 ? */
long edi; /* 目标变址 ? */
long es; /* 附加数据段寄存器 */ /* 16 high bits zero */
long cs; /* 代码段寄存器 */ /* 16 high bits zero */
long ss; /* 堆栈段选择符 */ /* 16 high bits zero */
long ds; /* 数据段寄存器 */ /* 16 high bits zero */
long fs; /* 附加数据段寄存器 */ /* 16 high bits zero */
long gs; /* 附加数据段寄存器 */ /* 16 high bits zero */
long ldt; /* LDT 段选择符 */ /* 16 high bits zero */
long trace_bitmap; /* I/O 位图基地址字段 */ /* bits: trace 0, bitmap 16-31 */
struct i387_struct i387; /* */
};
struct i387_struct {
long cwd;
long swd;
long twd;
long fip;
long fcs;
long foo;
long fos;
long st_space[20]; /* 8*10 bytes for each FP-reg = 80 bytes */
};
struct task_struct {
/* these are hardcoded - don't touch */
/* 任务的运行状态(-1 不可运行,0 可运行(就绪), >0 已停止) */
long state; /* -1 unrunnable, 0 runnable, >0 stopped */
/* 任务运行时间计数(递减)(滴答数),运行时间片 */
long counter;
/* 运行优先数。任务开始运行时 counter = priority,越大运行越长 */
long priority;
/* 信号。是位图,每个比特位代表一种信号,信号值=位偏移值+1 */
long signal;
/* 信号执行属性结构,对应信号将要执行的操作和标志信息 */
struct sigaction sigaction[32];
/* 进程信号屏蔽码(对应信号位图) */
long blocked; /* bitmap of masked signals */
/* various fields */
/* 任务执行停止的退出码,其父进程会取 */
int exit_code;
/* 代码段地址 */
unsigned long start_code;
/* 代码长度(字节数) */
unsinged long end_code;
/* 代码长度 + 数据长度(字节数) */
unsigned long end_data;
/* 总长度(字节数) */
unsigned long brk;
/* 堆栈段地址 */
unsigned long start_stack;
/* 进程标识号(进程号) */
long pid;
/* 父进程号 */
long father;
/* 进程组号 */
long pgrp;
/* 会话号 */
long session;
/* 会话首领 */
long leader;
/* 用户标识号(用户 id) */
unsigned short uid;
/* 有效用户 id */
unsigned short euid;
/* 保存的用户 id */
unsigned short suid;
/* 组标识号(组 id) */
unsigned short gid;
/* 有效组 id */
unsigned short egid;
/* 保存的组 id */
unsigned short sgid;
/* 报警定时值(滴答数) */
long alarm;
/* 用户态运行时间(滴答数) */
long utime;
/* 系统态运行时间(滴答数) */
long stime;
/* 子进程用于态运行时间 */
long cutime;
/* 子集成系统态运行时间 */
long cstime;
/* 进程开始运行时刻 */
long start_time;
/* 标志:是否使用了协处理器 */
unsigned short used_math;
/* file system info */
/* 进程使用 tty 的子设备号。-1 表示没有使用 */
int tty; /* -1 if no tty, so it must be signed */
/* 文件创建属性屏蔽位 */
unsigned short umask;
/* 当前工作目录 i 节点结构 */
struct m_inode * pwd;
/* 根目录 i 节点结构 */
struct m_inode * root;
/* 执行文件 i 节点结构 */
struct m_inode * executable;
/* 执行时关闭文件句柄位图标志 */
unsigned long close_on_exec;
/* 进程使用的文件表结构 */
struct file * filp[NR_OPEN];
/* 本任务的局部表描述符。 0 空, 1 代码段 cs, 2 数据段和堆栈段 ds&ss */
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];
/* 本进程的任务状态段信息结构 */
/* tss for this task */
struct tss_struct tss;
};
fork() 函数在main.c中定义如下:
// init/main.c
static inline _syscall0(int,fork)
而 _syscall0 是一个宏定义,其实现如下:
// include/unistd.h
#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; \
}
宏定义中 ## 为连接符,即 _NR##name 将替换成 __NR_fork ,这也是一个宏定义:
// include/unistd.h
#define __NR_fork 2
所以 _syscall0(int,fork) 展开后为:
static inline int fork(void) {
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (2));
if (__res >= 0)
return (type) __res;
errno = -__res;
return -1;
}
从上可知,其定义了 fork 函数,其通过 0x80 中断进入系统调用。 0x80 对应的中断实现为 system_call 函数,其在 sched.c 文件中有定义,如下:
// kernel/sched.c
void sched_init(void)
{
// ...
set_system_gate(0x80,&system_call);
}
系统调用(通常称为 syscalls)是 Linux 内核与上层应用程序进行交互通信的唯一接口,参见图 5-4 所示。从对中断机制的说明可知,用户程序通过直接或间接(通过库函数)调用中断 int 0x80,并在 eax 寄存器中指定系统调用功能号,即可使用内核资源,包括系统硬件资源。不过通常应用程序都是使用具有标准接口定义的 C 函数库中的函数间接地使用内核的系统调用,见图 5-19 所示。
通常系统调用使用函数形式进行调用,因此可带有一个或多个参数。对于系统调用执行的结果,它会在返回值中表示出来。通常负值表示错误,而 0 则表示成功。在出错的情况下,错误的类型码被存放在全局变量 errno 中。通过调用库函数 perror(),我们可以打印出该错误码对应的出错字符串信息。
在 Linux 内核中,每个系统调用都具有唯一的一个系统调用功能号。这些功能号定义在文件 include/unistd.h 中第 60 行开始处。例如,write 系统调用的功能号是 4,定义为符号 ___NR_write 。这些系统调用功能号实际上对应于 include/linux/sys.h 中定义的系统调用处理程序指针数组表 sys_call_table[] 中项的索引值。因此 write() 系统调用的处理程序指针就位于该数组的项 4 处。
另外,我们从 sys_call_table[] 中可以看出,内核中所有系统调用处理函数的名称基本上都是以符号 ‘sys_’ 开始的。例如系统调用 read()在内核原代码中的实现函数就是 sys_read()。
当应用程序经过库函数向内核发出一个中断调用 int 0x80 时,就开始执行一个系统调用。其中寄存器 eax 中存放着系统调用号,而携带的参数可依次存放在寄存器 ebx、ecx 和 edx 中。因此 Linux 0.11 内核中用户程序能够向内核最多直接传递三个参数,当然也可以不带参数。处理系统调用中断 int 0x80 的过程是程序 kernel/system_call.s 中的 system_call 。
为了方便执行系统调用,内核源代码在 include/unistd.h 文件( 133-183 行)中定义了宏函数 _syscalln(),其中 n 代表携带的参数个数,可以分别 0 至 3。因此最多可以直接传递 3 个参数。若需要传递大块数据给内核,则可以传递这块数据的指针值。例如对于 read()系统调用,其定义是:
int read(int fd, char *buf, int n);
若我们在用户程序中直接执行对应的系统调用,那么该系统调用的宏的形式为:
#define __LIBRARY__
#include
_syscall3(int, read, int, fd, char *, buf, int, n)
因此我们可以在用户程序中直接使用上面的 _syscall3()来执行一个系统调用 read(),而不用通过 C 函数库作中介。实际上 C 函数库中函数最终调用系统调用的形式和这里给出的完全一样。
对于 include/unistd.h 中给出的每个系统调用宏,都有 2+2*n 个参数。其中第 1 个参数对应系统调用返回值的类型;第 2 个参数是系统调用的名称;随后是系统调用所携带参数的类型和名称。这个宏会被
扩展成包含内嵌汇编语句的 C 函数,见如下所示。
可以看出,这个宏经过展开就是一个读操作系统调用的具体实现。其中使用了嵌入汇编语句以功能
号 _NR_read(3) 执行了 Linux 的系统中断调用 0x80。该中断调用在 eax(__res)寄存器中返回了实际读取的字节数。若返回的值小于 0,则表示此次读操作出错,于是将出错号取反后存入全局变量 errno 中,并向调用程序返回-1 值。
当进入内核中的系统调用处理程序 kernel/system_call.s 后,system_call 的代码会首先检查 eax 中的系统调用功能号是否在有效系统调用号范围内,然后根据 sys_call_table[]函数指针表调用执行相应的系统调用处理程序。
call _sys_call_table(,%eax, 4) // kernel/system_call.s 第 94 行。
这句汇编语句操作数的含义是间接调用地址在_sys_call_table + %eax * 4 处的函数。由于 sys_call_table[] 指针每项 4 个字节,因此这里需要给系统调用功能号乘上 4。然后用所得到的值从表中获取被调用处理函数的地址。
关于 Linux 用户进程向系统中断调用过程传递参数方面,Linux 系统使用了通用寄存器传递方法,例如寄存器 ebx、ecx 和 edx。这种使用寄存器传递参数方法的一个明显优点就是:当进入系统中断服务程序而保存寄存器值时,这些传递参数的寄存器也被自动地放在了内核态堆栈上,因此用不着再专门对传递参数的寄存器进行特殊处理。这种方法是 Linus 当时所知的最简单最快速的参数专递方法。另外还有一种使用 Intel CPU 提供的系统调用门(System Call gate)的参数传递方法,它在进程用户态堆栈和内核态堆栈自动复制传递的参数。但这种方法使用起来步骤比较复杂。
另外,在每个系统调用处理函数中应该对传递的参数进行验证,以保证所有参数都合法有效。尤其是用户提供的指针,应该进行严格地审查。以保证指针所指的内存区域范围有效,并且具有相应的读写权限。
对于系统调用( int 0x80 )的中断处理过程,可以把它看作是一个"接口"程序。实际上每个系统调用功能的处理过程基本上都是通过调用相应的 C 函数进行的。即所谓的 “Bottom half” 函数。
这个程序在刚进入时会首先检查 eax 中的功能号是否有效(在给定的范围内),然后保存一些会用到的寄存器到堆栈上。Linux 内核默认地把 段寄存器 ds,es 用于 内核数据段,而 fs 用于 用户数据段。接着通过一个地址跳转表( sys_call_table )调用相应系统调用的 C 函数。在 C 函数返回后,程序就把返回值压入堆栈保存起来。
接下来,该程序查看执行本次调用进程的状态。如果由于上面 C 函数的操作或其他情况而使进程的状态从执行态变成了其他状态,或者由于时间片已经用完(counter==0),则调用进程调度函数 schedule()(jmp _schedule) 。由于在执行 “jmp _schedule” 之前已经把返回地址 ret_from_sys_call 入栈,因此在执行完 schedule() 后最终会返回到 ret_from_sys_call 处继续执行。
从 ret_from_sys_call 标号处开始的代码执行一些系统调用的后处理工作。主要判断当前进程是否是初始进程 0,如果是就直接退出此次系统调用中断返回。否则再根据代码段描述符和所使用的堆栈来判断本次统调用的进程是否是一个普通进程,若不是则说明是内核进程(例如初始进程 1 )或其他。则也立刻弹出堆栈内容退出系统调用中断。末端的一块代码用来处理调用系统调用进程的信号。若进程结构的信号位图表明该进程有接收到信号,则调用信号处理函数 do_signal()。
最后,该程序恢复保存的寄存器内容,退出此次中断处理过程并返回调用程序。若有信号时则程序会首先"返回"到相应信号处理函数中去执行,然后返回调用 system_call 的程序。
system_call 函数定义在 kernel/system_call.s 文件中
# kernel/system_call.s
.globl system_call,sys_fork,timer_interrupt,sys_execve
.globl hd_interrupt,floppy_interrupt,parallel_interrupt
.globl device_not_available, coprocessor_error
.align 2
bad_sys_call:
movl $-1,%eax
iret
.align 2
reschedule:
pushl $ret_from_sys_call
jmp schedule
.align 2
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call *sys_call_table(,%eax,4) # 调用地址sys_call_table + 4 * %eax
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
其会调用 sys_call_table 中预定义的函数,此处即为 sys_fork 。
// include/linux/sys.h
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid, sys_iam, sys_whoami };
sys_fork 函数定义在 system_call.s中。
# 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 函数找到空闲的进程(内核中定义了64个,NR_TASKS),其返回内部进程序列。然后调用 copy_process 函数。
该函数为新进程取得不重复的进程号。函数返回在任务数组中的任务号。
// 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++) // 任务0项,因为常驻,所以不使用
if (!task[i])
return i;
return -EAGAIN;
}
用于创建并复制进程的代码段和数据段以及环境。在进程复制过程中,工作主要牵涉到进程数据结构中信息的设置。系统首先为新建进程在主内存区中申请一页内存来存放其任务数据结构信息,并复制当前进程任务数据结构中的所有内容作为新进程任务数据结构的模板。
// 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;
// NOTE!: the following statement now work with gcc 4.3.2 now, and you
// must compile _THIS_ memcpy without no -O of gcc.#ifndef GCC4_3
*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;
}
该函数复制内存页表。参数 nr 是新任务号,p 是新任务数据结构指针。该函数为新任务在线性地址空间中设置代码段和数据段基址、限长,并复制页表。由于Linux采用了写时复制技术,因此这里仅为新进程设置自己的页目录表项和页表项,而没有实际为新进程分配物理内存页面。此时新进程与其父进程共享所有内存页面。
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x0f);
data_limit=get_limit(0x17);
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000;
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}