Linux内核创建一个进程的过程分析
作者:网络转载 发布时间:[ 2016/3/18 15:39:22 ] 推荐标签:操作系统 Linux
不管在什么系统中,所有的任务都是以进程为载体的,所以理解进程的创建对于理解操作系统的原理是非常重要的,本文是我在学习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、引发0×80中断
进程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 0×80中断,调用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(),该函数主要负责以下的内容。
为子进程创建task_struct,将父进程的task_struct复制给子进程。
为子进程的task_struct,tss做个性化设置。
为子进程创建第一个页表,也将父进程的页表内容赋给这个页表。
子进程共享父进程的文件。
设置子进程的GDT项。
后将子进程设置为绪状态,使其可以参与进程间的轮转。
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; // 返回新进程号(与任务号是不同的)。
}
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11