进程的初始化与创建

main.c入口

void main_rename(void)		/* 这里确实是void,并没错。 */
{			/* 在startup 程序(head.s)中就是这样假设的。 */
/*
 * 此时中断仍被禁止着,做完必要的设置后就将其开启。
 */
	// 下面这段代码用于保存:
	// 根设备号 -> ROOT_DEV; 高速缓存末端地址 -> buffer_memory_end;
	// 机器内存数 -> memory_end;主内存开始地址 -> main_memory_start;
 	ROOT_DEV = ORIG_ROOT_DEV;
 	drive_info = DRIVE_INFO;
	memory_end = (1<<20) + (EXT_MEM_K<<10);// 内存大小=1Mb 字节+扩展内存(k)*1024 字节。
	memory_end &= 0xfffff000;			// 忽略不到4Kb(1 页)的内存数。
	if (memory_end > 16*1024*1024)		// 如果内存超过16Mb,则按16Mb 计。
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024)		// 如果内存>12Mb,则设置缓冲区末端=4Mb
		buffer_memory_end = 4*1024*1024;
	else if (memory_end > 6*1024*1024)	// 否则如果内存>6Mb,则设置缓冲区末端=2Mb
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;// 否则则设置缓冲区末端=1Mb
	main_memory_start = buffer_memory_end;// 主内存起始位置=缓冲区末端;
#ifdef RAMDISK	// 如果定义了虚拟盘,则主内存将减少。
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
// 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,实在看
// 不下去了,就先放一放,看下一个初始化调用-- 这是经验之谈:)
	mem_init(main_memory_start,memory_end);
	trap_init();	// 陷阱门(硬件中断向量)初始化。(kernel/traps.c)
	blk_dev_init();	// 块设备初始化。(kernel/blk_dev/ll_rw_blk.c)
	chr_dev_init();	// 字符设备初始化。(kernel/chr_dev/tty_io.c)空,为以后扩展做准备。
	tty_init();		// tty 初始化。(kernel/chr_dev/tty_io.c)
	time_init();	// 设置开机启动时间 -> startup_time。
	sched_init();	// 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c)
	buffer_init(buffer_memory_end);// 缓冲管理初始化,建内存链表等。(fs/buffer.c)
	hd_init();		// 硬盘初始化。(kernel/blk_dev/hd.c)
	floppy_init();	// 软驱初始化。(kernel/blk_dev/floppy.c)
	sti();			// 所有初始化工作都做完了,开启中断。

// 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0。
	move_to_user_mode();	// 移到用户模式。(include/asm/system.h)
	if (!fork()) {		/* we count on this going ok */
		init();
	}
/*
 * 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
 * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任
 * 务0 在任何空闲时间里都会被激活(当没有其它任务在运行时),
 * 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运行,如果没
 * 有的话我们就回到这里,一直循环执行'pause()'。
 */
	for(;;) pause();
} // end main

在main.c文件里,可以看到在main_rename函数里,有内存的一些操作,还有各种初始化

看看sched_init()调度程序初始化

void sched_init (void)
{
	int i;
	struct desc_struct *p;	// 描述符表结构指针。

	if (sizeof (struct sigaction) != 16)	// sigaction 是存放有关信号状态的结构。
		panic ("Struct sigaction MUST be 16 bytes");
// 设置初始任务(任务0)的任务状态段描述符和局部数据表描述符(include/asm/system.h,65)。
	set_tss_desc (gdt + FIRST_TSS_ENTRY, &(init_task.task.tss));
	set_ldt_desc (gdt + FIRST_LDT_ENTRY, &(init_task.task.ldt));
// 清任务数组和描述符表项(注意i=1 开始,所以初始任务的描述符还在)。
	p = gdt + 2 + FIRST_TSS_ENTRY;
	for (i = 1; i < NR_TASKS; i++)
	{
		task[i] = NULL;
		p->a = p->b = 0;
		p++;
		p->a = p->b = 0;
		p++;
	}
/* 清除标志寄存器中的位NT,这样以后就不会有麻烦 */
// NT 标志用于控制程序的递归调用(Nested Task)。当NT 置位时,那么当前中断任务执行
// iret 指令时就会引起任务切换。NT 指出TSS 中的back_link 字段是否有效。
//  __asm__ ("pushfl ; andl $0xffffbfff,(%esp) ; popfl");	// 复位NT 标志。
	_asm pushfd; _asm and dword ptr ss:[esp],0xffffbfff; _asm popfd;
	ltr (0);			// 将任务0 的TSS 加载到任务寄存器tr。
	lldt (0);			// 将局部描述符表加载到局部描述符表寄存器。
// 注意!!是将GDT 中相应LDT 描述符的选择符加载到ldtr。只明确加载这一次,以后新任务
// LDT 的加载,是CPU 根据TSS 中的LDT 项自动加载。
// 下面代码用于初始化8253 定时器。
	outb_p (0x36, 0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p (LATCH & 0xff, 0x40);	/* LSB */// 定时值低字节。
	outb (LATCH >> 8, 0x40);	/* MSB */// 定时值高字节。
  // 设置时钟中断处理程序句柄(设置时钟中断门)。
	set_intr_gate (0x20, &timer_interrupt);
  // 修改中断控制器屏蔽码,允许时钟中断。
	outb (inb_p (0x21) & ~0x01, 0x21);
  // 设置系统调用中断门。
	set_system_gate (0x80, &system_call);
}

因为CPU再执行调度程序,就是把此时CPU中的一些值,返回到当前进程的TSS和LDT上,然后将将要调用进程的TSS和LDT赋值到CPU中进行取指读值。

后面的for循环就是清空除0外到64的进程

最后可以看到它设置了一个set_system_gate函数,也就是之前提到过的,是中断特权级为所有程序都能调用的函数。而传进去的是system_call。

进程的切换,进程的调度,进程的创建,都是通过system_call,也就是系统调用来完成的。

 

而看上面代码

通过move_to_user_mode();将内核态转换为用户态。是因为在进行上面各个初始化的时候,避免cpu被强占,于是使用内核态,而后面就使用用户态就ok

 

在fork()方法里面就会手动创建第一个进程,也就是0号进程,然后0号进程会执行init()方法

if (!fork()) {		/* we count on this going ok */
		init();
	}

​
void init(void)
{
	int pid,i;

// 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。
// 该函数是在25 行上的宏定义的,对应函数是sys_setup(),在kernel/blk_drv/hd.c。
	setup((void *) &drive_info);

	(void) open("/dev/tty0",O_RDWR,0);	// 用读写访问方式打开设备“/dev/tty0”,
										// 这里对应终端控制台。
										// 返回的句柄号0 -- stdin 标准输入设备。
	(void) dup(0);		// 复制句柄,产生句柄1 号-- stdout 标准输出设备。
	(void) dup(0);		// 复制句柄,产生句柄2 号-- stderr 标准出错输出设备。
	printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS, \
		NR_BUFFERS*BLOCK_SIZE);	// 打印缓冲区块数和总字节数,每块1024 字节。
	printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);//空闲内存字节数。

// 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值,
// 对于原(父进程)将返回子进程的进程号。所以if (!(pid=fork())) {...} 内是子进程执行的内容。
// 该子进程关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和
// 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。
	if (!(pid=fork())) {
		close(0);
		if (open("/etc/rc",O_RDONLY,0))
			_exit(1);	// 如果打开文件失败,则退出(/lib/_exit.c)。
		execve("/bin/sh",argv_rc,envp_rc);	// 装入/bin/sh 程序并执行。(/lib/execve.c)
		_exit(2);	// 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。
	}

// 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的
// 进程号(pid)。这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的
// 位置。如果wait()返回值不等于子进程号,则继续等待。
	if (pid>0)
		while (pid != wait(&i))
		{	/* nothing */;}

// --
// 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建
// 一个子进程,如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对
// 于所创建的子进程关闭所有以前还遗留的句柄(stdin, stdout, stderr),新创建一个
// 会话并设置进程组号,然后重新打开/dev/tty0 作为stdin,并复制成stdout 和stderr。
// 再次执行系统解释程序/bin/sh。但这次执行所选用的参数和环境数组另选了一套(见上面)。
// 然后父进程再次运行wait()等待。如果子进程又停止了执行,则在标准输出上显示出错信息
//		“子进程pid 停止了运行,返回码是i”,
// 然后继续重试下去…,形成“大”死循环。
	while (1) {
		if ((pid=fork())<0) {
			printf("Fork failed in init\r\n");
			continue;
		}
		if (!pid) {
			close(0);close(1);close(2);
			setsid();
			(void) open("/dev/tty0",O_RDWR,0);
			(void) dup(0);
			(void) dup(0);
			_exit(execve("/bin/sh",argv,envp));
		}
		while (1)
			if (pid == wait(&i))
				break;
		printf("\n\rchild %d died with code %04x\n\r",pid,i);
		sync();
	}
	_exit(0);	/* NOTE! _exit, not exit() */
}

​

进程的初始化

在0号进程中:

            1.打开标准输入 输出 错误的控制台句柄

            2.创建1号进程,如果创建成功,则在1号进程中首先打开了"/etc/rc"文件,执行SHELL程序"/bin/sh"

            3.0号进程不可能结束,他会在没有其他进程调用的时候调用,只会执行for(;;) pause();

 

进程创建

             fork

                1.在task链表中找一个进程空位存放当前的进程

                2.创建一个task_struct

                3.设置task_struct

进程的创建就是对0号进程或者当前进程的复制:

    结构体的复制  把task[0]对应的task_struct复制给新创建的task_struct

    对于栈堆的拷贝   当进程做创建的时候要复制原有的栈堆

_sys_fork:
	call _find_empty_process ;// 调用find_empty_process()(kernel/fork.c,135)。
	test eax,eax
	js l2
	push gs
	push esi
	push edi
	push ebp
	push eax
	call _copy_process ;// 调用C 函数copy_process()(kernel/fork.c,68)。
	add esp,20 ;// 丢弃这里所有压栈内容。
l2: ret

首先会call 这个 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;
}

然后压入一些创建进程需要的参数到寄存器里,这个不太懂。

然后会call,这个copy_process函数

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;
	struct i387_struct *p_i387;

	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;
// 如果当前任务使用了协处理器,就保存其上下文。
	p_i387 = &p->tss.i387;
	if (last_task_used_math == current)
	_asm{
		mov ebx, p_i387
		clts
		fnsave [p_i387]
	}
//    __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;		// 返回新进程号(与任务号是不同的)。
}

1.nr就是上个函数(find_empty_process)返回出来的进程号,后面的参数都是一些寄存器的值

2.进程的创建主体:

          struct  task_struct *p;

          p = (struct task_struct *) get_free_page ();

就是给这个结构体分配内存,因为是linux 0.11版本,所以用这个函数

而现在的版本可能就是用 malloc(sizeof(task_struct));

3.将当前的子进程放入到整体进程链表中

                      task[nr]=p;

4.设置创建的task_struct结构体,也就是初始化。

 

 

// 设置新任务的代码和数据段基址、限长并复制页表。
// nr 为新任务号;p 是新任务数据结构的指针。
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;
}

把老的代码段和数据段拷贝到此进程的新的代码段和数据段

可以看到这里是给新的代码段和数据段都分配了地址,而且加到了LDT里面,再进行拷贝

其他的可以看看注释

最后返回新进程号

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值