前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >linux内核进程创建fork源码解析

linux内核进程创建fork源码解析

作者头像
用户4415180
发布2022-06-23 14:28:29
8.6K0
发布2022-06-23 14:28:29
举报
文章被收录于专栏:高并发高并发

    平时写过多进程多线程程序,比如使用linux的系统调用fork创建子进程和glibc中的nptl包里的pthread_create创建线程,甚至在java里使用Thread类创建线程等,虽然使用问题不大,但需要知道底层原理。这次在自己写操作系统的时候,看了一遍linux内核的进程创建过程。算是有了比较深入的理解。

    进程概念:进程是对正在运行程序的一个抽象。一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器、和变量的当前值,文件描述符等。从概念上说,每个进程拥有它自己的虚拟cpu。

    线程概念:线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针,寄存器集合,堆栈等,线程创建速度快,因为线程和所属进程共享资源,避免了资源复制和重新创建的开销。在linux下线程属于轻量级进程,拥有完全一样的数据结构,是系统调度的最小单位。并且线程和cpu是1:1模型,也就是说当前cpu在一个时间片周期内只运行一个线程,这样可以充分利用硬件。

   看下进程和线程结构体struct task_struct,由于此结构体成员很多只分析比较重要的成员。

代码语言:javascript
复制
/*
 * 进程结构体,同时也是轻量级进程(线程)结构体,基本调度单位
 * 由于结构体成员很多,只看关键成员
 */
struct task_struct {
	/*进程状态 -1 不能运行错误状态, 0 可以运行,在待运行队列, >0表示进程停止,比如在等待队列 */
	volatile long state;
	/*内核栈信息*/
	struct thread_info *thread_info;
	unsigned long flags;	/* 进程标志 */
	unsigned long ptrace;  //追踪标志


#ifdef CONFIG_SMP
#ifdef __ARCH_WANT_UNLOCKED_CTXSW
	int oncpu;   //在哪个cpu上运行
#endif
#endif
	//smp架构下负载权重
	int load_weight;	/* for niceness load balancing purposes */
	//优先级,静态优先级,普通优先级
	int prio, static_prio, normal_prio;
	//运行队列
	struct list_head run_list;
	//优先级队列指针,在进程调度算法中用到
	struct prio_array *array;
	//IO优先级
	unsigned short ioprio;
	unsigned int btrace_seq;
	//进程平均休眠时间
	unsigned long sleep_avg;
	unsigned long long timestamp, last_ran;
	unsigned long long sched_time; /* sched_clock time spent running */
	enum sleep_type sleep_type;

	unsigned long policy;
	cpumask_t cpus_allowed;
	//进程的时间片
	unsigned int time_slice, first_time_slice;


	struct list_head tasks;
	/*
	 * ptrace_list/ptrace_children forms the list of my children
	 * that were stolen by a ptracer.
	 */
	struct list_head ptrace_children;
	struct list_head ptrace_list;
	//内存描述符结构,如果为内核线程mm为null,只用active_mm,并且active_mm使用上个进程的mm
	struct mm_struct *mm, *active_mm;

/* task state */
	//二进制文件格式结构
	struct linux_binfmt *binfmt;
	//进程推出状态
	long exit_state;
	//退出码,退出信号
	int exit_code, exit_signal;
	/* ??? */
	unsigned long personality;
	//当前进程是否执行了二进制文件
	unsigned did_exec:1;
	//进程的pid,每个task_struct结构体唯一
	pid_t pid;
	//线程组id,如果当前task_struct是一个线程,则该值是所属进程的id
	//getpid也是返回的此值
	pid_t tgid;
	/* 
	 *进程的真实父进程,如果进程P的父进程不存在(比如退出),就指向1号init进程(由内核创建)
	 */
	struct task_struct *real_parent; /* real parent process (when being debugged) */
	 /*
	  *当前进程的父进程
	  */
	struct task_struct *parent;	/* parent process */
	/*
	 *当前进程的子进程列表
	 */
	struct list_head children;	/* list of my children */
	struct list_head sibling;	/* 当前进程的兄弟进程列表*/
	struct task_struct *group_leader;	/* 线程组领导,如果当前进程是线程,就是创建该线程的进程 */

	/* pid 哈希表,通过pid查找进程结构. */
	struct pid_link pids[PIDTYPE_MAX];
	/*线程组链表*/
	struct list_head thread_group;

	struct completion *vfork_done;		/* vfork()使用的结构 */
	int __user *set_child_tid;		/* CLONE_CHILD_SETTID */
	int __user *clear_child_tid;		/* CLONE_CHILD_CLEARTID */

	unsigned long rt_priority; //实时进程优先级

	/*执行命令,比如./a.out*/
	char comm[TASK_COMM_LEN]; /* executable name excluding path
				     - access with [gs]et_task_comm (which lock
				       it with task_lock())
				     - initialized normally by flush_old_exec */
    /* cpu寄存器的状态 */
	struct thread_struct thread;
    /* f文件系统信息 */
	struct fs_struct *fs;
   /* 打开的文件描述符信息 */
	struct files_struct *files;
   /* 命名空间信息 */
	struct namespace *namespace;
   /* 信号处理 */
	struct signal_struct *signal;
	struct sighand_struct *sighand;
};

其中一部分是进程资源相关的,比如mm, active_mm, fs,files,namespace,signal等成员,一部分和调度相关的比如sleep_avg,time_slice,prio,static_prio等。再看其中三个比较重要的结构:

struct thread_info 字面意思是线程信息,其实主要是内核栈的信息,每个进程都有自己的内核栈和用户栈,还可以设置中断栈,其中和进程上下文切换相关的主要是内核栈。

代码语言:javascript
复制
struct thread_info {
	struct task_struct	*task;		/* 内核栈所对应的进程指针*/
	struct exec_domain	*exec_domain;	/* 执行域,比如64位系统执行32位程序 */
	unsigned long		flags;		/* 标志位 */
	unsigned long		status;		/* 线程同步标志 */
	__u32			cpu;		/* 当前cpu */
	int			preempt_count;	/* 内核抢占标记,0表示可以抢占,大于0表示不能抢占,小于0错误*/
	//线程地址空间
	mm_segment_t		addr_limit;	/* thread address space:
					 	   0-0xBFFFFFFF for user-thead
						   0-0xFFFFFFFF for kernel-thread
						*/
	void			*sysenter_return;
	struct restart_block    restart_block;

	//前一个堆栈的esp,比如中断嵌套时
	unsigned long           previous_esp;   /* ESP of the previous stack in case
						   of nested (IRQ) stacks
						*/
	//0数组,表示内核栈的起始地址
	__u8			supervisor_stack[0];
};

此结构实现的很精妙,栈底表示thread_info结构,但也有危险,内核栈大小默认8KB,如果嵌套过多,可能会导致爆栈,所以内核态编程禁止使用递归。此结构如下图:

struct thread_info的起始地址要8KB对齐,在进入内核态后,会将用户态堆栈切换为内核态堆栈 ,这样我们就可以根据当前栈指针获取struct thread_info结构体,进而获取当前进程的task_struct指针,也就是有名的current宏,下面看如何获取的

代码语言:javascript
复制
/* 寄存器变量,表示当前栈指针esp寄存器*/
register unsigned long current_stack_pointer asm("esp") __attribute_used__;

/* 获取thread_info结构体指针*/
static inline struct thread_info *current_thread_info(void)
{
	/*
	 *THREAD_SIZE = 8KB, thread_info的起始地址要8KB对齐,这样就变成esp & 0xFFFFE000
	 *比如thread_info起始地址是0x4000, 栈顶为0x6000,当前esp为0x5110,则0x5110 & 0xFFFFE000 = 0x4000
     */
	return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1));
}

//获取current指针
static __always_inline struct task_struct * get_current(void)
{
	return current_thread_info()->task;
}
 
#define current get_current()

第二个重要的结构是struct thread_struct结构体表示cpu寄存器相关信息,此结构体在进程上下文切换时有用,被挂起进程的eip esp cs等寄存器值会存在此结构,表示如下:

代码语言:javascript
复制
/*thread_struct特定cpu寄存器信息,在进程上下文切换时有用*/
struct thread_struct {
/* TLS描述符 */
	struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];
	unsigned long	esp0;      
	unsigned long	sysenter_cs;//内核代码段寄存器值
	unsigned long	eip;   //内核eip寄存器值
	unsigned long	esp;   //内核栈指针
	unsigned long	fs;    //fs寄存器值
	unsigned long	gs;    //gs寄存器值
/* Hardware debugging registers */ //debug寄存器信息
	unsigned long	debugreg[8];  /* %%db0-7 debug registers */
/* fault info */
	unsigned long	cr2, trap_no, error_code; //cr2缺页地址,trap_no异常号,error_code错误码
/* floating point info */
	union i387_union	i387;   //浮点寄存器信息
/* virtual 86 mode info */
	struct vm86_struct __user * vm86_info;
	unsigned long		screen_bitmap;
	unsigned long		v86flags, v86mask, saved_esp0;
	unsigned int		saved_fs, saved_gs;
/* IO permissions */
	unsigned long	*io_bitmap_ptr;//IO权限指针
 	unsigned long	iopl;
/* max allowed port in the bitmap, in bytes: */
	unsigned long	io_bitmap_max; //IO位图
};

  第三个是struct mm_struct,表示内存描述符,此描述符主要描述了进程的虚拟地址空间信息。结构如下:

代码语言:javascript
复制
struct mm_struct {
	//vma链表的起始结构
	struct vm_area_struct * mmap;		/* list of VMAs */
	struct rb_root mm_rb;        //红黑树根节点主要组织vma
	struct vm_area_struct * mmap_cache;	/* last find_vma result,上次调用find vma的结果缓存 */
	//获取未映射区域地址
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
	void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
	//mmap区基地址
	unsigned long mmap_base;		/* base of mmap area */
	//用户进程虚拟空间大小
	unsigned long task_size;		/* size of task vm space */
	//空洞空间大小
	unsigned long cached_hole_size;         /* if non-zero, the largest hole below free_area_cache */
	//内核从这个地址搜索进程地址空间中的空闲区
	unsigned long free_area_cache;		/* first hole of size cached_hole_size or larger */
	//页目录指针
	pgd_t * pgd;
	//使用此mm的用户数
	atomic_t mm_users;			/* How many users with user space? */
	//mm的引用次数
	atomic_t mm_count;			/* How many references to "struct mm_struct" (users count as 1) */
	//属于此mm的vma数量
	int map_count;				/* number of VMAs */
	//读写信号量
	struct rw_semaphore mmap_sem;

	//页表锁
	spinlock_t page_table_lock;		/* Protects page tables and some counters */

	//mm结构链表
	struct list_head mmlist;		/* List of maybe swapped mm's.  These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */

	/* Special counters, in some configurations protected by the
	 * page_table_lock, in other configurations by being atomic.
	 */
	mm_counter_t _file_rss;   //分配给文件的页框数
	mm_counter_t _anon_rss;  //分配给匿名页的页框数

	//进程所拥有的最大页框数
	unsigned long hiwater_rss;	/* High-watermark of RSS usage */
	//进程所拥有的最大页数
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	//分别表示进程地址空间页数,锁住的页数,共享文件内存映射的页数,可执行内存映射的页数
	unsigned long total_vm, locked_vm, shared_vm, exec_vm;

	//分别表示栈空间页框数,保留页数,默认标志位,页表项数量
	unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;

	//分别表示进程代码段起始地址,结束地址,数据段起始地址,结束地址
	unsigned long start_code, end_code, start_data, end_data;

	//分别表示进程堆起始地址,结束地址,栈起始地址
	unsigned long start_brk, brk, start_stack;

	//分别表示进程参数起始地址,结束地址,环境变量起始地址,结束地址
	unsigned long arg_start, arg_end, env_start, env_end;

	//开始执行程序时使用
	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

	unsigned dumpable:2;
	cpumask_t cpu_vm_mask;

	/* 特定结构的mm上下文 */
	mm_context_t context;

	/* 进程将在这个时间有资格获得交换标记 */
	unsigned long swap_token_time;
	//如果最近发生了缺页中断,则设置该标志
	char recent_pagein;

	/* 正在把进程地址空间的内容卸载到转储文件中的轻量级进程数量 */
	int core_waiters;
	//转储原语
	struct completion *core_startup_done, core_done;

	/* 异步io锁 */
	rwlock_t		ioctx_list_lock;
	//异步io上下文链表
	struct kioctx		*ioctx_list;
};

重要的结构体介绍完了,可以看创建流程了,进程线程的创建都要调用同一个函数就是do_fork, 系统调用sys_fork,sys_clone,和内核线程的创建kernel_thread函数最终都要调用do_fork。

代码语言:javascript
复制
/*
 *  fork进程的主要函数,sys_fork,sys_clone等用户系统调用和kernel_thread创建内核线程函数都会调用
 *  此函数。也就是说不管是进程还是线程创建最终都会进入此函数。在这不管是线程还是进程统一用进程
 *  描述。
 *  clone_flags: fork进程标志
 *  stack_start: 新进程的栈起始地址
 *  regs:进行调用前保存的各个寄存器的值,比如从用户态进入内核态保存在栈中的各个寄存器的值
 *  stack_size:新进程的栈大小
 *  parent_tidptr:当创建线程时,表示父进程的用户态变量地址
 *  child_tidptr:当创建线程时,表示新线程的用户态变量地址
 */
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p;
	int trace = 0;          //内核追踪标志
	struct pid *pid = alloc_pid();   //分配一个pid结构,struct pid的nr成员表示进程号
	long nr;

	if (!pid)
		return -EAGAIN;
	nr = pid->nr;        //进程号
	if (unlikely(current->ptrace)) {    
		trace = fork_traceflag (clone_flags);
		if (trace)
			clone_flags |= CLONE_PTRACE;
	}

	//进程复制核心函数
	p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, nr);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) { //如果进程复制没有出错
		struct completion vfork;

		if (clone_flags & CLONE_VFORK) {//vfork标志
			p->vfork_done = &vfork;
			init_completion(&vfork);
		}

		if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) {
			/*
			 * We'll start up with an immediate SIGSTOP.
			 */
			sigaddset(&p->pending.signal, SIGSTOP);
			set_tsk_thread_flag(p, TIF_SIGPENDING);
		}

		//如果没有CLONE_STOPPED标志,则调用wake_up_new_task将新进程加入可运行队列
		if (!(clone_flags & CLONE_STOPPED))
			wake_up_new_task(p, clone_flags);
		else
			p->state = TASK_STOPPED; //否则将新进程设置为停止

		if (unlikely (trace)) {
			current->ptrace_message = nr;
			ptrace_notify ((trace << 8) | SIGTRAP);
		}

		if (clone_flags & CLONE_VFORK) { //如果是VFORK标志则需要先等待新进程执行完
			wait_for_completion(&vfork);
			if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
				current->ptrace_message = nr;
				ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
			}
		}
	} else {
		free_pid(pid);
		nr = PTR_ERR(p);
	}
	return nr;  //返回新进程的pid
}

其中最主要的copy过程全部交给了copy_process,此函数复制了所有进程资源信息,下面看此函数

代码语言:javascript
复制
/*
 * clone_flags:fork标志
 * stack_start:新进程栈起始地址
 * regs:调用时保存的各个寄存器值
 * stack_size: 新进程栈大小
 * parent_tidptr:创建线程时,父进程的用户态变量指针
 * child_tidptr:创建线程时,新线程的用户态变量指针
 * pid: 要创建的新进程分配的pid
 */
static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					struct pt_regs *regs,
					unsigned long stack_size,
					int __user *parent_tidptr,
					int __user *child_tidptr,
					int pid)
{
	int retval;
	struct task_struct *p = NULL;
	/*标志检查,表示不能同时设置这两个标志,CLONE_NEWNS表示要创建一个自己的命名空间,也就是
	 *即自己挂载的文件系统,而CLONE_FS表示和父进程共享目录,所以矛盾
	 */
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);

	/*
	 * CLONE_THREAD表示将子进程插入到父进程同一线程组中,并且必须共享父进程的信号描述符,
	 * 所以和!(clone_flags & CLONE_SIGHAND)矛盾
	 */
	if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
		return ERR_PTR(-EINVAL);

	/*
	 *  如果共享信号描述符,则必须共享内存空间,所以和!(clone_flags & CLONE_VM)矛盾
	 *
	 */
	if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
		return ERR_PTR(-EINVAL);
	//安全相关检查
	retval = security_task_create(clone_flags);
	if (retval)
		goto fork_out;

	retval = -ENOMEM;

	//创建新进程struct task_struct指针
	p = dup_task_struct(current);
	if (!p)
		goto fork_out;

#ifdef CONFIG_TRACE_IRQFLAGS
	DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
	DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
	retval = -EAGAIN;
	//判断当前用户进程数是否超过阈值
	if (atomic_read(&p->user->processes) >=
			p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
		if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
				p->user != &root_user)
			goto bad_fork_free;
	}

	//新进程用户引用次数加一
	atomic_inc(&p->user->__count);
	//新进程用户进程数加一
	atomic_inc(&p->user->processes);
	get_group_info(p->group_info);

	/*
	 * If multiple threads are within copy_process(), then this check
	 * triggers too late. This doesn't hurt, the check is only there
	 * to stop root fork bombs.
	 */
	 /*
	  *判断系统中线程数是否超过最大线程数,此变量max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE);
	  *在fork_init函数初始化,比如4GB的内存则最大线程数为65536
	  */
	if (nr_threads >= max_threads)
		goto bad_fork_cleanup_count;

	if (!try_module_get(task_thread_info(p)->exec_domain->module))
		goto bad_fork_cleanup_count;

	if (p->binfmt && !try_module_get(p->binfmt->module))
		goto bad_fork_cleanup_put_domain;

	//加载可执行文件标志置为0
	p->did_exec = 0;
	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
	copy_flags(clone_flags, p); //复制进程标志flags
	p->pid = pid;   //给新进程赋值进程号pid
	retval = -EFAULT;
	//如果是设置了CLONE_PARENT_SETTID标志,则将子进程的pid复制给父进程的parent_tidptr
	if (clone_flags & CLONE_PARENT_SETTID)
		if (put_user(p->pid, parent_tidptr))
			goto bad_fork_cleanup_delays_binfmt;

	//初始化新进程的子进程链表
	INIT_LIST_HEAD(&p->children);
	//初始化新进程的兄弟进程链表
	INIT_LIST_HEAD(&p->sibling);
	p->vfork_done = NULL;
	spin_lock_init(&p->alloc_lock);

	clear_tsk_thread_flag(p, TIF_SIGPENDING);
	init_sigpending(&p->pending);

	//以下都是初始化一些成员变量
	p->utime = cputime_zero;
	p->stime = cputime_zero;
 	p->sched_time = 0;
	p->rchar = 0;		/* I/O counter: bytes read */
	p->wchar = 0;		/* I/O counter: bytes written */
	p->syscr = 0;		/* I/O counter: read syscalls */
	p->syscw = 0;		/* I/O counter: write syscalls */
	acct_clear_integrals(p);

   ...........


	//将新进程pid复制给新进程tgid
	p->tgid = p->pid;
	//如果设置CLONE_THREAD标志,说明创建的是线程,则将父进程的tgid复制给新进程的tgid,说明获取线程
	//所属的进程id需要获取tgid成员
	if (clone_flags & CLONE_THREAD)
		p->tgid = current->tgid;

	if ((retval = security_task_alloc(p)))  //安全相关检查
		goto bad_fork_cleanup_policy;
	if ((retval = audit_alloc(p)))          //审计检查
		goto bad_fork_cleanup_security;
	/* 以下开始复制所有资源*/
	if ((retval = copy_semundo(clone_flags, p)))//i386下为空
		goto bad_fork_cleanup_audit;
	if ((retval = copy_files(clone_flags, p)))  //复制打开的文件描述符
		goto bad_fork_cleanup_semundo;
	if ((retval = copy_fs(clone_flags, p)))    //复制文件路径
		goto bad_fork_cleanup_files;
	if ((retval = copy_sighand(clone_flags, p)))  //复制信号处理
		goto bad_fork_cleanup_fs;
	if ((retval = copy_signal(clone_flags, p)))  //复制信号
		goto bad_fork_cleanup_sighand;
	if ((retval = copy_mm(clone_flags, p)))    //复制内存描述符
		goto bad_fork_cleanup_signal;
	if ((retval = copy_keys(clone_flags, p)))  //i386下为空
		goto bad_fork_cleanup_mm;
	if ((retval = copy_namespace(clone_flags, p))) //复制命名空间
		goto bad_fork_cleanup_keys;
	//复制进程上下文相关信息
	retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
	if (retval)
		goto bad_fork_cleanup_namespace;
	//子进程在用户态下的指针,CLONE_CHILD_SETTID设置了
	p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
	/*
	 * Clear TID on mm_release()?
	 */
	p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
	p->robust_list = NULL;
#ifdef CONFIG_COMPAT
	p->compat_robust_list = NULL;
#endif
	INIT_LIST_HEAD(&p->pi_state_list);
	p->pi_state_cache = NULL;

	/*
	 * sigaltstack should be cleared when sharing the same VM
	 */
	if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
		p->sas_ss_sp = p->sas_ss_size = 0;

	/*
	 * Syscall tracing should be turned off in the child regardless
	 * of CLONE_PTRACE.
	 */
	clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
	clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif

	/* Our parent execution domain becomes current domain
	   These must match for thread signalling to apply */
	   
	p->parent_exec_id = p->self_exec_id;

	/* ok, now we should be set up.. */
	p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
	p->pdeath_signal = 0;
	p->exit_state = 0;

	/*
	 * Ok, make it visible to the rest of the system.
	 * We dont wake it up yet.
	 */
	p->group_leader = p; //将新进程的线程组领导进程设置为自己
	INIT_LIST_HEAD(&p->thread_group); //初始化线程组链表
	INIT_LIST_HEAD(&p->ptrace_children);//初始化追踪子进程链表
	INIT_LIST_HEAD(&p->ptrace_list);  //初始化追踪链表

	/* Perform scheduler related setup. Assign this task to a CPU. */
	sched_fork(p, clone_flags);   //如果是smp系统则给新进程指定cpu

	/* Need tasklist lock for parent etc handling! */
	write_lock_irq(&tasklist_lock);


	p->cpus_allowed = current->cpus_allowed;
	if (unlikely(!cpu_isset(task_cpu(p), p->cpus_allowed) ||
			!cpu_online(task_cpu(p))))
		set_task_cpu(p, smp_processor_id());

	/* 如果设置CLONE_PARENT 或 CLONE_THREAD则新进程的真实父进程和父进程的真实父进程一样*/
	if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
		p->real_parent = current->real_parent;
	else
		p->real_parent = current; //否则新进程的真实父进程就是当前进程
	p->parent = p->real_parent;  //新进程的父进程是新进程的真实父进程

	spin_lock(&current->sighand->siglock);

	/*
	 * Process group and session signals need to be delivered to just the
	 * parent before the fork or both the parent and the child after the
	 * fork. Restart if a signal comes in before we add the new process to
	 * it's process group.
	 * A fatal signal pending means that current will exit, so the new
	 * thread can't slip out of an OOM kill (or normal SIGKILL).
 	 */
 	recalc_sigpending();
	if (signal_pending(current)) {
		spin_unlock(&current->sighand->siglock);
		write_unlock_irq(&tasklist_lock);
		retval = -ERESTARTNOINTR;
		goto bad_fork_cleanup_namespace;
	}

	//如果新进程是线程,则将父进程的组领导复制给新进程组领导,如果是进程则组领导是新进程本身
	if (clone_flags & CLONE_THREAD) {
		p->group_leader = current->group_leader;
		list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);

		if (!cputime_eq(current->signal->it_virt_expires,
				cputime_zero) ||
		    !cputime_eq(current->signal->it_prof_expires,
				cputime_zero) ||
		    current->signal->rlim[RLIMIT_CPU].rlim_cur != RLIM_INFINITY ||
		    !list_empty(&current->signal->cpu_timers[0]) ||
		    !list_empty(&current->signal->cpu_timers[1]) ||
		    !list_empty(&current->signal->cpu_timers[2])) {
			/*
			 * Have child wake up on its first tick to check
			 * for process CPU timers.
			 */
			p->it_prof_expires = jiffies_to_cputime(1);
		}
	}

	/*
	 * 继承父进程的IO优先级
	 */
	p->ioprio = current->ioprio;

	if (likely(p->pid)) { //如果新进程pid有效
		add_parent(p);   //将新进程加入到兄弟进程链表
		if (unlikely(p->ptrace & PT_PTRACED))
			__ptrace_link(p, current->parent);

		//如果新进程p是线程组领导,也就是创建的是进程,则将父进程的一些资源复制给新进程
		if (thread_group_leader(p)) {   
			p->signal->tty = current->signal->tty;
            //将当前进程的进程组ID复制给新进程进程组ID,新进程老进程都指向同一个进程组
			p->signal->pgrp = process_group(current);
            //将当前进程的回话ID复制给新进程,新老进程同属一个回话ID
			p->signal->session = current->signal->session;
			attach_pid(p, PIDTYPE_PGID, process_group(p));
			attach_pid(p, PIDTYPE_SID, p->signal->session);

			list_add_tail_rcu(&p->tasks, &init_task.tasks);
			__get_cpu_var(process_counts)++;
		}
		attach_pid(p, PIDTYPE_PID, p->pid);
		nr_threads++;
	}

	total_forks++;   //fork次数加一
	spin_unlock(&current->sighand->siglock);
	write_unlock_irq(&tasklist_lock);
	proc_fork_connector(p);
	return p;   //copy成功返回新进程的指针


/*以下是出错处理*/
bad_fork_cleanup_namespace:
	exit_namespace(p);
bad_fork_cleanup_keys:
	exit_keys(p);
bad_fork_cleanup_mm:
	if (p->mm)
		mmput(p->mm);
bad_fork_cleanup_signal:
	cleanup_signal(p);
bad_fork_cleanup_sighand:
	__cleanup_sighand(p->sighand);

............
............

bad_fork_free:
	free_task(p);
fork_out:
	return ERR_PTR(retval);
}

此函数中调用的最主要的函数为dup_task_struct,copy_files,copy_fs,copy_sighand,copy_signal,copy_mm,copy_namespace,copy_thread。在处理进程线程的pgid,tgid,group_leader,parent时也有区别,pgid是进程组ID,tgid是线程组ID,group_leader是线程组领导进程,parent是父进程,如果创建的是进程则group_leader是新进程本身,pgid是当前进程(创建子进程的进程)的pgid,tgid是新进程本身,parent是当前进程(创建子进程的进程)。如果创建是的线程则group_leader是当前进程(创建线程的进程)的group_leader,pgid是当前进程的pgid,tgid是当前进程的tgid,parent是当前进程的parent。其中copy_files,copy_fs,copy_sighand,copy_signal,copy_namespace处理流程差不多,都是判断是否有CLONE_XXX标志,如果有则和父进程公用同一个描述符,如果没有则新分配然后初始化。只分析copy_files。然后重点分析dup_task_struct,copy_mm,copy_thread。

copy_files是复制父进程打开的文件描述符,流程如下:

代码语言:javascript
复制
static inline int copy_fs(unsigned long clone_flags, struct task_struct * tsk)
{
	if (clone_flags & CLONE_FS) { //如果设置了CLONE_FS标志则不修改新进程的此成员和父进程一样
		atomic_inc(&current->fs->count);
		return 0;
	}
	//否则给新进程分配文件描述符对象,然后将父进程的描述符,复制给新进程的文件描述符
	tsk->fs = __copy_fs_struct(current->fs);
	if (!tsk->fs)
		return -ENOMEM;
	return 0;
}

下面看dup_task_struct,分配新进程的task_struct结构体并且做一些初始化。

代码语言:javascript
复制
static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
{
	//将父进程的thread_info信息复制给新进程的thread_info
	*task_thread_info(p) = *task_thread_info(org);
	task_thread_info(p)->task = p; //将新进程thread_info的task指针,指向新进程的task地址
}
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
	struct task_struct *tsk;   //新进程指针
	struct thread_info *ti;    //新进程thread_info指针

	prepare_to_copy(orig); //体系结构相关函数,i386为空

	tsk = alloc_task_struct();  //通过slab cache分配进程对象
	if (!tsk) 
		return NULL;

	ti = alloc_thread_info(tsk);  //分配thread_info对象
	if (!ti) {
		free_task_struct(tsk);  //如果分配失败则释放新进程对象
		return NULL;
	}

	*tsk = *orig;              /*
	  							*首先将父进程的task_struct结构体各个成员复制给新进程,有需要变化的成员
	  							*下面再修改,无需变化的则不用管
	  							*/
	tsk->thread_info = ti;      //将新进程thread_info结构指向新的thread_info
	setup_thread_stack(tsk, orig);  //设置新进程的内核栈

	/* One for us, one for whoever does the "release_task()" (usually parent) */
	atomic_set(&tsk->usage,2);     
	atomic_set(&tsk->fs_excl, 0);
	tsk->btrace_seq = 0;
	tsk->splice_pipe = NULL;
	return tsk;
}

主要是通过slab分配器,分配一个task_struct结构体,并将父进程的成员信息,复制给新进程,然后设置新进程的内核栈。

再看最重要的函数copy_mm,顾名思义复制内存空间,虚拟内存技术是现代cpu和操作系统的精华所在,重点分析下此函数。

代码语言:javascript
复制
/*
 *  clone_flags:clone标志
 *  tsk:新进程结构体指针
 */

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
	struct mm_struct * mm, *oldmm;//新进程mm和父进程mm
	int retval;

	tsk->min_flt = tsk->maj_flt = 0;
	tsk->nvcsw = tsk->nivcsw = 0;

	tsk->mm = NULL;         //新进程mm初始化为NULL
	tsk->active_mm = NULL;  //新进程active_mm也初始化为NULL

	/*
	 * Are we cloning a kernel thread?
	 *
	 * We need to steal a active VM for that..
	 */
	oldmm = current->mm;   //oldmm为当前进程的内存描述符
	if (!oldmm)            //如果当前进程的mm为null说明当前进程是内核线程,直接返回
		return 0;

	if (clone_flags & CLONE_VM) {   //如果CLONE标志有CLONE_VM,说明要共享虚拟内存
		atomic_inc(&oldmm->mm_users); //当前进程的mm描述符用户数加一
		mm = oldmm;     //新进程的mm描述符等于父进程的描述符,说明两个进程共享虚拟内存,线程就是这样
		goto good_mm;   //跳转到goto_mm
	}

	retval = -ENOMEM;
	//如果不共享虚拟内存空间,则需要创建一个新的内存描述符,并将父进程的mm有关信息复制到子进程
	mm = dup_mm(tsk);  
	if (!mm)
		goto fail_nomem;

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;

fail_nomem:
	return retval;
}

从此函数也可以看出,线程和进程的一个重要区别是,是否共享虚拟内存空间,如果创建的是线程则直接把父进程的mm引用,给新线程,如果是进程则需要复制一份内存空间给新进程,所以创建线程消耗要小很多,接下来看dup_mm函数。

代码语言:javascript
复制
/*
 * tsk:新进程结构体指针
 */
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
	struct mm_struct *mm, *oldmm = current->mm;//新进程mm和当前进程mm
	int err;

	if (!oldmm)
		return NULL; //如果当前进程mm为NULL则返回

	mm = allocate_mm(); //给新进程分配mm对象,通过slab cache分配器分配的
	if (!mm)
		goto fail_nomem;   //如果内存出错跳转到fail_nomem

	memcpy(mm, oldmm, sizeof(*mm));//将当前进程的mm所有信息,复制给新进程mm

	if (!mm_init(mm))          //初始化新进程mm
		goto fail_nomem;

	if (init_new_context(tsk, mm)) //初始化mm上下文,在x86架构下主要是复制LDT(局部描述符表)
		goto fail_nocontext;

	err = dup_mmap(mm, oldmm); //复制vma和页表项
	if (err)
		goto free_pt;

	mm->hiwater_rss = get_mm_rss(mm);
	mm->hiwater_vm = mm->total_vm;

	return mm;

free_pt:
	mmput(mm);

fail_nomem:
	return NULL;

fail_nocontext:
	/*
	 * If init_new_context() failed, we cannot use mmput() to free the mm
	 * because it calls destroy_context()
	 */
	mm_free_pgd(mm);
	free_mm(mm);
	return NULL;
}

此函数主要为新进程分配内存描述符,初始化一些属性,然后调用dup_mmap复制vma和页表,下面看mm_init

代码语言:javascript
复制
static struct mm_struct * mm_init(struct mm_struct * mm)
{
	atomic_set(&mm->mm_users, 1);   //新进程mm的用户数初始化为1
	atomic_set(&mm->mm_count, 1);   //新进程mm引用次数初始化为1
	init_rwsem(&mm->mmap_sem);     //初始化mmap信号量
	INIT_LIST_HEAD(&mm->mmlist);   //初始化mmlist链表
	mm->core_waiters = 0;
	mm->nr_ptes = 0;              //页表项初始化为0
	set_mm_counter(mm, file_rss, 0); //文件映射页初始化为0
	set_mm_counter(mm, anon_rss, 0); //匿名映射页初始化为0
	spin_lock_init(&mm->page_table_lock); //初始化mm自旋锁
	rwlock_init(&mm->ioctx_list_lock);  //初始化异步IO链表锁
	mm->ioctx_list = NULL;             //初始化异步IO链表
	mm->free_area_cache = TASK_UNMAPPED_BASE; //空闲区域为mmap起始地址,为1GB 0x40000000
	mm->cached_hole_size = ~0UL;    //空洞区域0XFFFFFFFF

	if (likely(!mm_alloc_pgd(mm))) {  //分配页目录对象
		mm->def_flags = 0;
		return mm;
	}
	free_mm(mm);
	return NULL;
}
static inline int mm_alloc_pgd(struct mm_struct * mm)
{
	mm->pgd = pgd_alloc(mm);  //分配页目录对象
	if (unlikely(!mm->pgd))   //如果内存不足,返回失败
		return -ENOMEM;
	return 0;
}
/*
 *  体系结构相关函数,x86 32位系统只有2级和3级页表,64位系统有4级页表,新版本linux的有5级页表
 *  其实页目录基地址就是一个unsigned long *指针,一共1024项
 */
pgd_t *pgd_alloc(struct mm_struct *mm)
{
	int i;
	pgd_t *pgd = kmem_cache_alloc(pgd_cache, GFP_KERNEL);//通过slab cache分配页目录对象

	if (PTRS_PER_PMD == 1 || !pgd)  //如果是二级页表也就是没PUD和PMD,则直接返回页目录对象
		return pgd;

	 //如果是三级页表,则分配PMD并设置页目录
	for (i = 0; i < USER_PTRS_PER_PGD; ++i) {
		pmd_t *pmd = kmem_cache_alloc(pmd_cache, GFP_KERNEL);
		if (!pmd)
			goto out_oom;
		set_pgd(&pgd[i], __pgd(1 + __pa(pmd)));
	}
	return pgd; //返回页目录对象

out_oom:
	//如果出错,则把分配的pmd释放
	for (i--; i >= 0; i--)
		kmem_cache_free(pmd_cache, (void *)__va(pgd_val(pgd[i])-1));
	kmem_cache_free(pgd_cache, pgd);//如果出错释放页目录对象
	return NULL;
}

mm_init主要做了初始化一些成员变量,分配页目录对象,并初始化页目录对象。

下面看重要的函数dup_mmap复制vma和页表,先介绍下linux的页表结构,linux支持四级页表,但是有的cpu mmu只支持两级页表或者三级页表,比如x86_32如果不开启PAE则只支持2级页表,开启PAE支持3级页表,x86_64支持四级页表,所以为了适应不同硬件,linux写了一个很巧妙的代码,在只支持二级页表的cpu中,pud和pmd的结果都是pgd,看以下代码

代码语言:javascript
复制
//在支持二级或三级页表的cpu中返回pgd
static inline pud_t * pud_offset(pgd_t * pgd, unsigned long address)
{
	return (pud_t *)pgd;
}
//在支持四级页表的cpu中返回真正的pud
#define pud_offset(pgd, address) ((pud_t *) pgd_page(*(pgd)) + pud_index(address))

x86_32不开启PAE的情况下支持二级页表,mmu翻译过程如下图

线性地址22到31位做为页目录偏移,定位到1024项页目录的其中某一项,然后取页目录的12到31位作为页表项基地址,线性地址的12到21位为偏移定位到具体的页表项,取页表项的12到31位作为内存页面的基地址加上 线性地址的0到11位作为偏移,定位到具体的物理地址。

x86_64支持四级页表,mmu翻译过程如下图:

和二级页表性质差不多,只不过分的更细,因为地址长度为48位。这里不再熬述。下面看具体的复制过程

代码语言:javascript
复制
/*
 * mm:新进程mm
 * oldmm: 当前进程mm
 * 主要功能复制vma,复制页表
*/
static inline int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
	struct vm_area_struct *mpnt, *tmp, **pprev;
	struct rb_node **rb_link, *rb_parent;
	int retval;
	unsigned long charge;
	struct mempolicy *pol;

	down_write(&oldmm->mmap_sem);
	flush_cache_mm(oldmm);   //体系结构相关,x86下为空实现
	/*
	 * Not linked in yet - no deadlock potential:
	 */
	down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING);

	mm->locked_vm = 0;
	mm->mmap = NULL;       //新mm起始vma为NULL
	mm->mmap_cache = NULL; //最近find_vma的结果为NULL
	//将当前进程的mmap区域基地址复制给新mm的free_area_cache
	mm->free_area_cache = oldmm->mmap_base;
	//cached_hole_size为0xFFFFFFFF,刚才已经赋值又重新赋值,手下误
	mm->cached_hole_size = ~0UL;
	//vma数量初始为0
	mm->map_count = 0;
	cpus_clear(mm->cpu_vm_mask);
	//初始化vma的红黑树根节点
	mm->mm_rb = RB_ROOT;
	rb_link = &mm->mm_rb.rb_node;
	rb_parent = NULL;
	pprev = &mm->mmap; //新mm的起始vma地址给pprev

	//遍历当前进程所有的vma
	for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
		struct file *file;

		if (mpnt->vm_flags & VM_DONTCOPY) {//如果此vma有不能copy标志,则统计一些信息后跳过
			long pages = vma_pages(mpnt);
			mm->total_vm -= pages;
			vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
								-pages);
			continue;
		}
		charge = 0;
		if (mpnt->vm_flags & VM_ACCOUNT) {
			unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
			if (security_vm_enough_memory(len))
				goto fail_nomem;
			charge = len;
		}
		//给新进程分配vma
		tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
		if (!tmp)
			goto fail_nomem;
		*tmp = *mpnt; //将当前进程此vma的所有属性复制给新进程的此vma
		pol = mpol_copy(vma_policy(mpnt));
		retval = PTR_ERR(pol);
		if (IS_ERR(pol))
			goto fail_nomem_policy;
		vma_set_policy(tmp, pol);
		tmp->vm_flags &= ~VM_LOCKED; //新进程vma去掉VM_LOCKED标志
		tmp->vm_mm = mm;        //将新进程的vma的内存描述符指向新进程mm
		tmp->vm_next = NULL;    //新进程vma的next为NULL
		anon_vma_link(tmp);     //如果是匿名vma,则加入匿名vma链表
		file = tmp->vm_file;    //新vma所对应的文件
		if (file) {       //如果此vma是映射的文件,则将当前进程的非线性映射,复制给新进程
			struct inode *inode = file->f_dentry->d_inode;
			get_file(file);
			if (tmp->vm_flags & VM_DENYWRITE)
				atomic_dec(&inode->i_writecount);
      
			/* insert tmp into the share list, just after mpnt */
			spin_lock(&file->f_mapping->i_mmap_lock);
			tmp->vm_truncate_count = mpnt->vm_truncate_count;
			flush_dcache_mmap_lock(file->f_mapping);
			vma_prio_tree_add(tmp, mpnt);
			flush_dcache_mmap_unlock(file->f_mapping);
			spin_unlock(&file->f_mapping->i_mmap_lock);
		}

		/*
		 * Link in the new vma and copy the page table entries.
		 */
		*pprev = tmp;  //将临时vma复制给新进程的mmap链表
		pprev = &tmp->vm_next; //pprev指向下一个地址

		//将新进程的vma插入红黑树
		__vma_link_rb(mm, tmp, rb_link, rb_parent);
		rb_link = &tmp->vm_rb.rb_right;
		rb_parent = &tmp->vm_rb;

		mm->map_count++;  //vma个数加一

		retval = copy_page_range(mm, oldmm, mpnt);//复制vma所有的页表项

		if (tmp->vm_ops && tmp->vm_ops->open)
			tmp->vm_ops->open(tmp);

		if (retval)
			goto out;
	}
	retval = 0;
out:
	up_write(&mm->mmap_sem);//释放信号量
	flush_tlb_mm(oldmm); //体系结构相关,x86为空实现
	up_write(&oldmm->mmap_sem);
	return retval;
fail_nomem_policy:
	kmem_cache_free(vm_area_cachep, tmp); //如果出错则释放刚才分配的vma
fail_nomem:
	retval = -ENOMEM;
	vm_unacct_memory(charge);
	goto out;
}

主要是vma的复制,页表项的复制在copy_page_range函数,看此函数和该函数调用的函数,可以细细品味,linux如何使用一套代码应对不同cpu2 3 4级页表复制时的策略。代码写的很巧妙,适配性很强。

代码语言:javascript
复制
/*
 *  将当前进程页表复制给新进程的页表
 *  dst_mm:新进程mm
 *  src_mm: 当前进程mm
 *  vma:当前进程的vma
 */
int copy_page_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
		struct vm_area_struct *vma)
{
	pgd_t *src_pgd, *dst_pgd; //当前进程页目录,新进程页目录
	unsigned long next;
	unsigned long addr = vma->vm_start; //vma线性区起始地址
	unsigned long end = vma->vm_end; //vma线性区结束地址

	//如果是巨页,则调用copy_hugetlb_page_range
	if (is_vm_hugetlb_page(vma))
		return copy_hugetlb_page_range(dst_mm, src_mm, vma);

	//普通页表复制
	dst_pgd = pgd_offset(dst_mm, addr); //addr对应的新页目录项指针
	src_pgd = pgd_offset(src_mm, addr); //addr对应的当前目录项指针
	do {
		/*
		 *next边界确定,如果是二级页表,一个页目录可以映射4MB,所以如果end - addr大于4MB,
		 *则next最大为addr + 4MB,否则为next = end
		 */
		next = pgd_addr_end(addr, end); 
		//如果硬件只支持二级页表,这项没用,非二级页表,则是判断页目录是否为NULL
		if (pgd_none_or_clear_bad(src_pgd))
			continue;
		//copy pud表,如果硬件只支持二级页表,则pud就是pgd
		if (copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
						vma, addr, next))
			return -ENOMEM;
	} while (dst_pgd++, src_pgd++, addr = next, addr != end);
	return 0;
}

下面开始pud的复制函数,如果是二级三级页表返回的还是pgd ,啥也不做

代码语言:javascript
复制
/*
 * 复制pud表,linux通用代码实现是4级页表,但是通过高超代码设计可以适配2 3 4级页表,可见代码质量很高,
 * 设计很巧妙
 */
static inline int copy_pud_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
		pgd_t *dst_pgd, pgd_t *src_pgd, struct vm_area_struct *vma,
		unsigned long addr, unsigned long end)
{
	pud_t *src_pud, *dst_pud; //当前进程pud表,新进程pud表
	unsigned long next;

	/*
	 *如果cpu支持四级页表,如果pgd有对应的pud,则返回pud表指针,否则分配一个pud
	 *如果cpu支持二级页表,则直接返回PGD
	 */
	dst_pud = pud_alloc(dst_mm, dst_pgd, addr);
	if (!dst_pud)
		return -ENOMEM;
	/*
	 * 如果是四级页表返回当前进程的pud表,如果是二级页表返回pgd
	 */
	src_pud = pud_offset(src_pgd, addr); 
	do {
		//确定next地址,如果是四级页表,则在x86_64架构下一项pud映射为1GB物理内存,所以
		//next的边界最大为addr + 1GB
		next = pud_addr_end(addr, end);
		if (pud_none_or_clear_bad(src_pud))
			continue;
		//复制pmd表
		if (copy_pmd_range(dst_mm, src_mm, dst_pud, src_pud,
						vma, addr, next))
			return -ENOMEM;
	} while (dst_pud++, src_pud++, addr = next, addr != end);
	return 0;
}

下面开始复制pmd,逻辑同上

代码语言:javascript
复制
static inline int copy_pmd_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
		pud_t *dst_pud, pud_t *src_pud, struct vm_area_struct *vma,
		unsigned long addr, unsigned long end)
{
	pmd_t *src_pmd, *dst_pmd;
	unsigned long next;

	/*
	 *如果cpu支持四级页表,如果pud有对应的pmd,则返回pmd表指针,否则分配一个pmd
	 *如果cpu支持二级页表,则直接返回PGD
	 *如果cpu支持三级页表,则返回PGD对应的PMD
	 */
	dst_pmd = pmd_alloc(dst_mm, dst_pud, addr);
	if (!dst_pmd)
		return -ENOMEM;
	/*
	 * 如果是三级或者四级页表返回当前进程的pmd表,如果是二级页表返回pgd
	 */
	src_pmd = pmd_offset(src_pud, addr);
	do {
		//确定next地址,x86_32开启PAE支持三级页表,则一项PMD映射2MB内存,x86_64支持四级页表
		//一项PMD映射也是映射2MB内存
		next = pmd_addr_end(addr, end);
		if (pmd_none_or_clear_bad(src_pmd))
			continue;
		//重要函数,复制最后一级页表项,234级页表都最重要的函数
		if (copy_pte_range(dst_mm, src_mm, dst_pmd, src_pmd,
						vma, addr, next))
			return -ENOMEM;
	} while (dst_pmd++, src_pmd++, addr = next, addr != end);
	return 0;
}

最重要的复制函数就是copy_pte_range,如下

代码语言:javascript
复制
/*
 * copy页表最终函数,也是最重要的函数,cpu不管支持2 3 4级页表都将在此函数完成最终
 * 复制
 */
static int copy_pte_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
		pmd_t *dst_pmd, pmd_t *src_pmd, struct vm_area_struct *vma,
		unsigned long addr, unsigned long end)
{
	pte_t *src_pte, *dst_pte;    //当前进程页表项,新进程页表项
	spinlock_t *src_ptl, *dst_ptl; //自旋锁
	int progress = 0;
	int rss[2];

again:
	rss[1] = rss[0] = 0;    //映射页表数
	//获取新进程pmd对应的页表项指针,如果为NULL则新分配一个
	dst_pte = pte_alloc_map_lock(dst_mm, dst_pmd, addr, &dst_ptl);
	if (!dst_pte)
		return -ENOMEM;
	//获取当前进程pmd对应的页表项指针
	src_pte = pte_offset_map_nested(src_pmd, addr);
	src_ptl = pte_lockptr(src_mm, src_pmd);
	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);

	//开始复制
	do {
		/*
		 * We are holding two locks at this point - either of them
		 * could generate latencies in another task on another CPU.
		 */
		if (progress >= 32) {//如果progess>=32
			progress = 0;
			/*
			 *因为如果页表项很多。复制很耗时间,所以如果有进程需要调度,则先跳出循环,去调度
			 *新进程,在下面cond_resched()后有一个goto again,也就是当前进程再次被调度执行
			 *的时候,会重新从打断的地方复制
			 */
			if (need_resched() ||
			    need_lockbreak(src_ptl) ||
			    need_lockbreak(dst_ptl))
				break;
		}
		if (pte_none(*src_pte)) { //如果页表项为null则跳过当前页表项
			progress++; //progress+1,因为此操作耗时短,所以只加一
			continue;
		}
		//如果pte不为NULL则开始真正复制pte
		copy_one_pte(dst_mm, src_mm, dst_pte, src_pte, vma, addr, rss);
		progress += 8;//copy_one_pte耗时稍微长,所以progress+8
	} while (dst_pte++, src_pte++, addr += PAGE_SIZE, addr != end);

	spin_unlock(src_ptl);
	pte_unmap_nested(src_pte - 1);
	add_mm_rss(dst_mm, rss[0], rss[1]);
	pte_unmap_unlock(dst_pte - 1, dst_ptl);
	cond_resched(); //调度其它进程
    //如果此进程被挂起了,再次恢复运行时,需要检查是否复制完,如果没有则接着复制
	if (addr != end)/
		goto again;
	return 0;
}

此函数有一个点很重要,就是在进行页表项复制时,如果页表项很多会很耗时间,如果此时有一个进程优先级很高,需要被调度,则我们不能等到复制完才去调度,这样会让用户难以忍受,或者如果是实时进程,则会出现问题,所以每复制四项,就去检查是否有需要被调度的进程,如果有,则立马进行调度。

下面看copy_one_pte函数,最终的复制函数

代码语言:javascript
复制
/*
 *   复制一个页表项
 *   dst_mm: 新进程的mm描述符
 *   src_mm: 当前进程mm描述符
 *   dst_pte:新进程的页表项
 *   src_pte:  当前进程页表项
 *   vma:当前进程vma
 *   addr:映射起始地址
 *   rss:映射页面数
 */
static inline void
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
		pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
		unsigned long addr, int *rss)
{
	unsigned long vm_flags = vma->vm_flags; //vma标志
	pte_t pte = *src_pte;   //当前进程pte表项临时变量
	struct page *page;  //页面指针

	/* pte contains position in swap or file, so copy. */
	if (unlikely(!pte_present(pte))) {  //如果pte对应的页框不在内存
		if (!pte_file(pte)) {  //如果pte映射的不是文件,则说明页框被换到了swap交换区
			swp_entry_t entry = pte_to_swp_entry(pte);

			swap_duplicate(entry);
			/* make sure dst_mm is on swapoff's mmlist. */
			//如果新进程的mmlist为空,则把新进程的mm添加到mm链表
			if (unlikely(list_empty(&dst_mm->mmlist))) {
				spin_lock(&mmlist_lock);
				if (list_empty(&dst_mm->mmlist))
					list_add(&dst_mm->mmlist,
						 &src_mm->mmlist);
				spin_unlock(&mmlist_lock);
			}
			//如果编译时配置了页面迁移,这个才有用
			if (is_write_migration_entry(entry) &&
					is_cow_mapping(vm_flags)) {
				/*
				 * COW mappings require pages in both parent
				 * and child to be set to read.
				 */
				make_migration_entry_read(&entry);
				pte = swp_entry_to_pte(entry);
				set_pte_at(src_mm, addr, src_pte, pte);
			}
		}
		goto out_set_pte;//如果是文件映射则直接跳到设置新页表项函数
	}

	/*
	 * If it's a COW mapping, write protect it both
	 * in the parent and the child
	 */
	 /*
	  *如果父进程此vma是写时复制,则将pte表项的写权限标志清除,这样在父进程或者子进程写
	  *数据的时候会触发缺页异常程序,然后缺页异常处理程序会判断是因为写时复制导致的,这样
	  *会为父进程或者子进程分配新的页面,并把旧页面的内容复制到新页面。这样做的好处是
	  * 减少开销,将复制操作延迟到了写数据的时候。
	  */
	if (is_cow_mapping(vm_flags)) {
		//清除父进程写权限
		ptep_set_wrprotect(src_mm, addr, src_pte);
		//清除子进程写权限
		pte = *src_pte;
	}

	/*
	 * If it's a shared mapping, mark it clean in
	 * the child
	 */
	 //如果vma具有共享标志,则将pte脏标志清除
	if (vm_flags & VM_SHARED)
		pte = pte_mkclean(pte);
	pte = pte_mkold(pte); //清除pte已经使用标志

	//获取父进程pte对应的物理页框
	page = vm_normal_page(vma, addr, pte);
	if (page) {
		get_page(page);//页框引用次数加一
		page_dup_rmap(page);//页框映射次数加一
		rss[!!PageAnon(page)]++; //匿名页或非匿名页映射加一
	}

out_set_pte:
	set_pte_at(dst_mm, addr, dst_pte, pte); //把修改后的pte复制给新进程pte
}

此函数最重要的一点就是对于可写的区,比如数据段,堆,栈等vma所对应的pte,需要设置写时复制,父子进程共享只读段,可以写的段需要独自拥有,但是可写段的数据复制要延迟到写发生的时候,这样可以提高效率,或者是避免不必要的操作,比如虽然数据段可写,但是接下来的代码直到进程结束没有发生写操作,这样我们就不必去复制页面了。另外fork函数也会快很多,所以有必要把写时复制延迟到写的时候在缺页处理函数中执行。进程 线程(轻量级进程)创建的主要函数已经讲完了,其中进程和线程的主要区别就是共享资源的问题,进程不共享任何资源,父子进程只会映射到相同的只读数据段,线程会共享fs(共享根目录和当前工作目录),files(打开文件描述符),mm(虚拟内存空间),SIGNAL(信号)等,所以线程创建要快很多,少去了很多资源的复制。

下面看最后一个函数,copy_thread主要复制cpu特定的进程上下文信息

代码语言:javascript
复制
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
	unsigned long unused,
	struct task_struct * p, struct pt_regs * regs)
{
	struct pt_regs * childregs; //子进程内核栈顶
	struct task_struct *tsk;
	int err;

	childregs = task_pt_regs(p);//获取子进程栈顶指针
	*childregs = *regs; //将父进程的内核栈帧结构复制给子进程内核栈帧
	childregs->eax = 0;  //调用完毕后创建进程完毕后子进程返回值
	childregs->esp = esp; //子进程的用户栈顶指针,在发生特权级切换时,内核栈会变成用户栈

	p->thread.esp = (unsigned long) childregs; //内核栈顶指针
	p->thread.esp0 = (unsigned long) (childregs+1);

	//新进程第一次被调度时,执行ret_from_fork汇编例程
	p->thread.eip = (unsigned long) ret_from_fork; 

	savesegment(fs,p->thread.fs); //保存fs段寄存器
	savesegment(gs,p->thread.gs);//保存gs段寄存器

	/*
	 * IO相关逻辑
	 */
	tsk = current;
	if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
		p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
		if (!p->thread.io_bitmap_ptr) {
			p->thread.io_bitmap_max = 0;
			return -ENOMEM;
		}
		memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
			IO_BITMAP_BYTES);
		set_tsk_thread_flag(p, TIF_IO_BITMAP);
	}

	/*
	 * Set a new TLS for the child thread?
	 */
	 //为子线程设置TLS
	if (clone_flags & CLONE_SETTLS) {
		struct desc_struct *desc;
		struct user_desc info;
		int idx;

		err = -EFAULT;
		if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
			goto out;
		err = -EINVAL;
		if (LDT_empty(&info))
			goto out;

		idx = info.entry_number;
		if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
			goto out;

		desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
		desc->a = LDT_entry_a(&info);
		desc->b = LDT_entry_b(&info);
	}

	err = 0;
 out:
 //IO位图相关操作
	if (err && p->thread.io_bitmap_ptr) {
		kfree(p->thread.io_bitmap_ptr);
		p->thread.io_bitmap_max = 0;
	}
	return err;
}

此函数最重要的就是内核栈的设置,把返回用户态的栈和地址都从父进程复制给了子进程,然后将子进程上下文切换用到的数据结构thread_struct的esp成员指向了子进程的内核栈。

创建进程线程主要流程图如下:

至此分析完毕。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018-08-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
轻量应用服务器
轻量应用服务器(TencentCloud Lighthouse)是新一代开箱即用、面向轻量应用场景的云服务器产品,助力中小企业和开发者便捷高效的在云端构建网站、Web应用、小程序/小游戏、游戏服、电商应用、云盘/图床和开发测试环境,相比普通云服务器更加简单易用且更贴近应用,以套餐形式整体售卖云资源并提供高带宽流量包,将热门开源软件打包实现一键构建应用,提供极简上云体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档