Linux内核创建一个进程的过程分析

2019-07-14 10:23发布

不管在什么系统中,所有的任务都是以进程为载体的,所以理解进程的创建对于理解操作系统的原理是非常重要的,本文是我在学习linux内核中所做的笔记,如有错误还请大家批评指正。注:我所阅读的内核版本是0.11。 一、关于PCB 对于一个进程来说,PCB就好像是他的记账先生,当一个进程被创建时PCB就被分配,然后有关进程的所有信息就全都存储在PCB中,例如,打开的文件,页表基址寄存器,进程号等等。在linux中PCB是用结构task_struct来表示的,我们首先来看一下task_struct的组成。 代码位于linux/include/linux/Sched.h struct task_struct { long state; //表示进程的状态,-1表示不可执行,0表示可执行,>0表示停止 long counter;/* 运行时间片,以jiffs递减计数 */ long priority; /* 运行优先数,开始时,counter = priority,值越大,表示优先数越高,等待时间越长. */ long signal;/* 信号.是一组位图,每一个bit代表一种信号. */ struct sigaction sigaction[32]; /* 信号响应的数据结构, 对应信号要执行的操作和标志信息 */ long blocked; /* 进程信号屏蔽码(对应信号位图) */ /* various fields */ int exit_code; /* 任务执行停止的退出码,其父进程会取 */ unsigned long start_code,end_code,end_data,brk,start_stack;/* start_code代码段地址,end_code代码长度(byte), end_data代码长度+数据长度(byte),brk总长度(byte),start_stack堆栈段地址 */ long pid,father,pgrp,session,leader;/* 进程号,父进程号 ,父进程组号,会话号,会话头(发起者)*/ unsigned short uid,euid,suid;/* 用户id 号,有效用户 id 号,保存用户 id 号*/ unsigned short gid,egid,sgid;/* 组标记号 (组id),有效组 id,保存的组id */ long alarm;/* 报警定时值 (jiffs数) */ long utime,stime,cutime,cstime,start_time;/* 用户态运行时间 (jiffs数), 系统态运行时间 (jiffs数),子进程用户态运行时间,子进程系统态运行时间,进程开始运行时刻 */ unsigned short used_math;/* 是否使用了协处理器 */ /* file system info */ int tty; /* 进程使用tty的子设备号. -1表示设有使用 */ unsigned short umask; /* 文件创建属性屏蔽位 */ struct m_inode * pwd; /* 当前工作目录 i节点结构 */ struct m_inode * root; /* 根目录i节点结构 */ struct m_inode * executable;/* 执行文件i节点结构 */ unsigned long close_on_exec; /* 执行时关闭文件句柄位图标志. */ struct file * filp[NR_OPEN]; /* 文件结构指针表,最多32项. 表项号即是文件描述符的值 */ struct desc_struct ldt[3]; /* 任务局部描述符表.0-空,1-cs段,2-Ds和Ss段 */ struct tss_struct tss; /* 进程的任务状态段信息结构 */ }; 二、进程的创建 系统中的进程是由父进程调用fork()函数来创建的,那么调用fork()函数的时候究竟会发生什么呢?

1、引发0x80中断

进程1是由进程0通过fork()创建的,其中的fork代码如下: init/main.c #define _syscall0(type,name) / type name(void) / { / long __res; / __asm__ volatile ( "int $0x80" / // 调用系统中断0x80。 :"=a" (__res) / // 返回值??eax(__res)。 :"0" (__NR_##name)); / // 输入为系统中断调用号__NR_name。 if (__res >= 0) / // 如果返回值>=0,则直接返回该值。 return (type) __res; errno = -__res; / // 否则置出错号,并返回-1。 return -1;} 这样使用int 0x80中断,调用sys_fork系统调用来创建进程。

2、sys_fork()

_sys_fork: call _find_empty_process # 调用find_empty_process()(kernel/fork.c,135)。 testl %eax,%eax js 1f push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process # 调用C 函数copy_process()(kernel/fork.c,68)。 addl $20,%esp # 丢弃这里所有压栈内容。 1: ret 虽然是一段汇编代码,但是我们可以很清楚的看到首先调用的是find_empty_process(),然后又调用了copy_process(),而这两个函数就是fork.c中的函数。下面我们来看一下这两个函数。

3、find_empty_process()

// 为新进程取得不重复的进程号last_pid,并返回在任务数组中的任务号(数组index)。 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; } find_empty_process的作用就是为所要创建的进程分配一个进程号。在内核中用全局变量last_pid来存放系统自开机以来累计的进程数,也将此变量用作新建进程的进程号。内核第一次遍历task[64],如果&&条件成立说明last_pid已经被别的进程使用了,所以++last_pid,直到获取到新的进程号。第二次遍历task[64],获得第一个空闲的i,也就是任务号。因为在linux0.11中,最多允许同时执行64个进程,所以如果当前的进程已满,就会返回-EAGAIN。

4、copy_process()

获得进程号并且将一些寄存器的值压栈后,开始执行copy_process(),该函数主要负责以下的内容。
  1. 为子进程创建task_struct,将父进程的task_struct复制给子进程。
  2. 为子进程的task_struct,tss做个性化设置。
  3. 为子进程创建第一个页表,也将父进程的页表内容赋给这个页表。
  4. 子进程共享父进程的文件。
  5. 设置子进程的GDT项。
  6. 最后将子进程设置为就绪状态,使其可以参与进程间的轮转。
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; // 将新任务结构指针放入任务数组中。 // 其中nr 为任务号,由前面find_empty_process()返回。 *p = *current; /* NOTE! this doesn't copy the supervisor stack */ /* 注意!这样做不会复制超级用户的堆栈 */ (只复制当前进程内容)。 p->state = TASK_UNINTERRUPTIBLE; // 将新进程的状态先置为不可中断等待状态。 p->pid = last_pid; // 新进程号。由前面调用find_empty_process()得到。 p->father = current->pid; // 设置父进程号。 p->counter = p->priority; p->signal = 0; // 信号位图置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; // 当前滴答数时间。 // 以下设置任务状态段TSS 所需的数据(参见列表后说明)。 p->tss.back_link = 0; p->tss.esp0 = PAGE_SIZE + (long) p; // 堆栈指针(由于是给任务结构p 分配了1 页 // 新内存,所以此时esp0 正好指向该页顶端)。 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; // 段寄存器仅16 位有效。 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); // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。 p->tss.trace_bitmap = 0x80000000; // 如果当前任务使用了协处理器,就保存其上下文。 if (last_task_used_math == current) __asm__ ("clts ; fnsave %0"::"m" (p->tss.i387)); // 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是0),则复位任务数组中 // 相应项并释放为该新任务分配的内存页。 if (copy_mem (nr, p)) { // 返回不为0 表示出错。 task[nr] = NULL; free_page ((long) p); return -EAGAIN; } // 如果父进程中有文件是打开的,则将对应文件的打开次数增1。 for (i = 0; i < NR_OPEN; i++) if (f = p->filp[i]) f->f_count++; // 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。 if (current->pwd) current->pwd->i_count++; if (current->root) current->root->i_count++; if (current->executable) current->executable->i_count++; // 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。 // 在任务切换时,任务寄存器tr 由CPU 自动加载。 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; // 返回新进程号(与任务号是不同的)。 } 进入copy_prossess函数后,调用get_free_page()函数,在主内存申请一个空闲页面,并将申请到的页面清0。将这个页面的指针强制类型转化成task_struct类型的指针,并挂接在task[nr]上,nr就是在find_empty_process中返回的任务号。 接下来的*p=*current将当前进程的指针赋给了子进程的,也就是说子进程继承了父进程一些重要的属性,当然这是不够的,所以接下来的一大堆代码都是为子进程做个性化设置的。 一般来讲,每个进程都要加载属于自己的代码、数据,所以copy_process设置子进程的内存地址。通过copy_mem来设置新任务的代码和数据段基址、限长并复制页表。 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) // 0.11 版不支持代码和数据段分立的情况。 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; // 新基址=任务号*64Mb(任务大小)。 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)) { // 复制代码和数据段。 free_page_tables (new_data_base, data_limit); // 如果出错则释放申请的内存。 return -ENOMEM; } return 0; } 然后是对文件,pwd等资源的修改,接着要设置子进程在GDT中的表项,最后将进程设置为就绪状态,并返回进程号。 三、创建过程总结 可以将上面繁琐的创建过程总结为一下的几步: 1、调用fork()函数引发0x80中断
2、调用sys_fork
3、通过find_empty_process为新进程分配一个进程号
4、通过copy_process函数使子进程复制父进程的资源,并进行一些个性化设置后,返回进程号。