Linux-----fork.c进行拆解分析

fork()函数说明

fork() 是一个用于创建新进程的系统调用,fork可以在父进程中创建一个子进程。子进程是父进程的副本,frok从父进程继承了大部分资源和状态。

先简单理解一下fork()函数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t pid; // 声明一个进程标识符变量

    // 使用 fork() 创建子进程
    pid = fork();

    if (pid < 0) {
        // fork() 失败
        perror("Fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        printf("This is the child process (PID=%d).\n", getpid());
    } else {
        // 父进程
        printf("This is the parent process (PID=%d) of the child process (PID=%d).\n", getpid(), pid);
    }

    // 父子进程都会执行以下代码
    printf("This is common code executed by both parent and child processes.\n");

    return 0;
}

linux0.11源码fork只有136行外层5个函数

#include <errno.h>//一组错误码
#include <linux/sched.h>//进程调度和进程管理struct task_struct。
#include <linux/kernel.h>//内核编程所需的通用功能
#include <asm/segment.h>//特定于架构的头文件
#include <asm/system.h> //特定于架构的头文件

//验证给定的物理内存地址是否可以进行写入操作
extern void write_verify(unsigned long address);//extern 关键字用于在一个源文件中引用其他源文件中定义的全局变量或函数。这允许不同的源文件共享全局变量和函数,以实现模块化和可维护的代码结构。

long last_pid=0;
//验证给定内存区域是否可以进行写入操作
void verify_area(void * addr,int size)
{
	unsigned long start;

	start = (unsigned long) addr;
	size += start & 0xfff;
	start &= 0xfffff000;
	start += get_base(current->ldt[2]);
	while (size>0) {
		size -= 4096;
		write_verify(start);
		start += 4096;
	}
}

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)) {
		free_page_tables(new_data_base,data_limit);
		return -ENOMEM;
	}
	return 0;
}

/*
 *  Ok, this is the main fork-routine. It copies the system process
 * information (task[nr]) and sets up the necessary registers. It
 * also copies the data segment in it's entirety.
 */
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;
	*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;
}

int find_empty_process(void)
{
	int i;//用于遍历进程数组
    // long last_pid = 0
	repeat://标签
		if ((++last_pid)<0) last_pid=1;//确保进程标识符为1
		for(i=0 ; i<NR_TASKS ; i++)
			if (task[i] && task[i]->pid == last_pid) goto repeat;//用于跳转到repeat标签
    //用于查找一个空闲的槽位,找到后返回i
	for(i=1 ; i<NR_TASKS ; i++)
		if (!task[i])
			return i;
	return -EAGAIN;//没有槽位则返回,说明不能找到task[]和task[]->pid 
}

int find_empty_process()拆解

功能:找到一个可用的进程槽位,以便在创建新进程时将其分配给新进程。它会遍历已有的进程表中的所有进程,检查它们的 PID 是否与当前分配的 PID 相同,如果相同就说明该 PID 已被占用,需要继续尝试分配下一个 PID。这个过程会一直重复,直到找到一个未被占用的 PID 为止。

int find_empty_process(void)
{
	int i;
    // long last_pid = 0
	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++)
		if (!task[i])
			return i;
	return -EAGAIN;
}

初始化变量i,用于循坏遍历进程数组,以及last_pid,用于保存上一个分配的进程标识符。
进入一个无限循坏(repeat:标签所在的位置),在循环中执行以下操作
使用++last_pid来递增last_pid变量的值,并检查是否小于0。如果last_pid变为负数,将其重新设置为1.这是为了确保进程标识符是正数。
接着,使用for循环来遍历任务数组task在task[i]不为空(表示槽位已被占用),且task[i]->pid等于last_pid(表示当前进程标识符已经被使用)时,跳转到repeat标签位置,继续尝试下一个标识符。
在循环中,通过检查task[i]是否为空来找到一个空闲的槽位。一旦找到一个空闲槽位,函数返回该槽位的索引i,表示已找到可用的进程槽位。
如果没有找到空间槽位,则函数返回-EAGAIN表示没有可用的进程槽位。

int copy_process(*)进行拆解

struct task_struct *p;//任务结构体
	int i;
	struct file *f;//文件结构体

	p = (struct task_struct *) get_free_page();//分配一个新的内存页面,表示新的进程块
	if (!p)
		return -EAGAIN;
	task[nr] = p;//指针存储在全局进程数组,跟踪新的进程
	*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);//局部占用表)指针_LDT(nr)存储在任务段(TSS)中
	p->tss.trace_bitmap = 0x80000000;
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));//如果当前进程上一次使用了FPU(浮点处理器单元),则清除浮点任务切换(TS)标志,然后保存FPU状态到新进程的TSS中。
	if (copy_mem(nr,p)) {//该函数用于复制当前进程的内存页表到新进程。如果复制失败,会释放新进程的资源并返回错误。
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)//查看当前进程的文件吞吐量filp队列,并递增相应文件的引用计数,以确保文件在新进程中仍然可用。
		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));//设置新的进程的 TSS 进程和 LDT 进程,以在全局进程表(GDT)中设置新的进程的 TSS 和 LDT。
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;	/* do this last, just in case *///最后,将新进程的状态设置为TASK_RUNNING,表示它已准备好运行,并返回分配给新进程的进程标识符last_pid。
	return last_pid;

对上默认字段再详细说明

__asm__("clts ; fnsave %0"::"m" (p->tss.i387));

这是内联汇编的标准__asm__用于执行特定的定制指令

  • clts:
    这是清除任务切换(TS)标志的x86指令。TS标志用于控制浮点任务切换。通过执行指令,将cltsTS标志清除,表示不使用浮点任务切换。

  • fnsave %0:这是x86设置指令,用于保存浮点状态注册到内存中。%0表示将操作数指定为内联总线语句的输出操作数,即p->tss.i387。

  • ::"m" (p->tss.i387):这是输入/输出操作数说明符。它告诉编译器如何将C代码中的变量与汇编代码中的操作数相匹配。"m":表示p->tss.i387是一个内存操作数,需要从内存中读取或写入数据。(p->tss.i387):指定了具体的变量,即浮点注册状态的存储位置。

int copy_mem(int nr,struct task_struct * 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);//get_limit函数获取,段限制表示的大小
	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)//这个条件检查是否支持独立了的指令和数据(I&D)段。如果父进程的代码段和数据段的基地址不同,就会引发panic,因为这段代码不支持这种情况。
		panic("We don't support separate I&D");
	if (data_limit < code_limit)//这个条件检查数据段的段限制是否小于代码段的段限制,如果是,会引发panic。这是因为数据段应该至少和代码段一样大,否则会出现不一致的情况
		panic("Bad data_limit");
	new_data_base = new_code_base = nr * 0x4000000;//这两个变量用于存储新进程(子进程)的代码段和数据段的基地址。这里通过简单的计算,为每个子进程分配一个4MB的地址空间,每个进程的地址空间都是nr*0x4000000
	p->start_code = new_code_base;//设置新进程的`start_code`字段为新的代码段基地址。这个字段用于记录代码段的起始位置。
	set_base(p->ldt[1], new_code_base);//这一行和下一行分别将新进程的局部描述符表(LDT)中的代码段和数据段的基地址设置为新的值,以便进程可以正确访问这些段。
	set_base(p->ldt[2], new_data_base);
    //这条语句调用'copy_page_tables'函数,将父进程的页表复制到子进程中,确保子进程可以访问与父进程相同的内存布局。
	if (copy_page_tables(old_data_base, new_data_base, data_limit))
	{  //如果页表复制失败,将释放已经分配的页表
		free_page_tables(new_data_base, data_limit);
		return -ENOMEM;
	}
//最终函数返回0表示完成进程的内存布局和页表复制。
	return 0;
}

void verify_area(void *addr, int size)

功能:用于验证指定内存区域的可访问性

void verify_area(void *addr, int size)
{
	unsigned long start;

	start = (unsigned long)addr;
	size += start & 0xfff;
	start &= 0xfffff000;
	start += get_base(current->ldt[2]);
	while (size > 0)
	{
		size -= 4096;
		write_verify(start);
		start += 4096;
	}
}

这个函数它接受两个参数,一个是指向内存地址的指针'addr',另一个是验证的内存大小size

  • unsigned long start:声明一个无符号长整型变量start,用于存储起始内存地址。

  • start=(unsigned long)addr;将转入的addr强制转换为无符号长整型,并将其赋值给start,以便对其进行操作。

  • size+=start & 0xfff:将size增加到start的低12位(0xfff)中。这是因为x86体系结构的内存分页方式,每个页的大小通常为4KB(2^12字节),这一行代码确保验证的范围包括整个页。

  • start &=0xfffff000;将start的低12位清零,以便获取当前页的基本地址。这将start对齐到4KB的页边界。

  • start +=get_base(current->ldt[2]);获取当前进程的局部描述符表(LDT)的第三个选择子(通常是数据段的选择子)的基地址,并将其添加到start上。这是为了将start转换位当前进程的线性地址。

  • 接下来的循坏,通过一个循坏,该循环以页大小(4KB)为步长逐渐验证内存区域。在每个迭代中,他将调用write_verity(start)来验证内存页的可访问性,然后将start增加4KB,继续验证下一页,直到验证完成整个内存区域

posted @ 2023-09-25 23:29  不会笑的孩子  阅读(95)  评论(0编辑  收藏  举报