Linux 进程管理与调度:(一)进程的创建与销毁

《Linux6.5源码分析:进程管理与调度系列文章》

本系列文章将对进程管理与调度进行知识梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中进程调度机制进行数据实时拿取与分析。

在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:

Linux 进程调度与管理:(一)进程的创建与销毁

在上一篇文章《进程调度与管理:(零)预备知识》中,我们简单介绍了一下关于进程调度的相关结构体,通过相关图片梳理了task_struct,sched_entity,rq,sched_avg之间的关系,为后面深入源码做了进出准备。本篇文章将继续围绕进程管理“周边”初步认识进程是如何从无到有的,又是如何“结束其辉煌一生”的,主要涉及到以下相关内容:

  • 进程的创建:fork,vfork,clone;
  • 进程的销毁:主动退出,被迫退出;

0.原理铺垫

有了相关结构体的铺垫,有利于我们阅读源码,但仅仅是阅读源码容易陷入源码细节中,所以需要再宏观上对进程的创建有一定的认识,本小节会对进程创建的原理与过程进行简要的介绍,主要涉及到:

  • 写时复制原理;
  • fork、vfork、clone流程与原理;

0.1.写时复制

在创建进程时,会复制父进程的task_struct结构体作为子进程的进程描述符,那么子进程是如何复制父进程的task_struct结构体的?如果复制父进程的全部内容,则会消耗大量的时间并占用大量的CPU及内存,显然是不合理的,写时复制技术可以解决这个问题。

写时复制技术就是父进程在创建子进程时不需要复制进程地址空间的内容到子进程,只需要复制父进程的进程地址空间的页表到子进程,这样父、子进程就共享了相同的物理内存。当父、子进程中有一方需要修改某个物理页面的内容时,触发写保护的缺页异常,然后才复制共享页面的内容,从而让父、子进程拥有各自的副本。也就是说,进程地址空间以只读的方式共享,当需要写入时才发生复制。写时复制是一种可以推迟甚至避免复制数据的技术,它在现代操作系统中有广泛的应用。

0.2.fork、vfork、clone

forkvforkclone均是用于创建进程的函数,他们通过系统调用在内核中实现,在Linux-6.5中,三者均是通过调用内核函数kernel_clone()实现,关于源码分析部分,会在下一章节详细剖析。下面将简略的介绍一下三者的区别:

fork()

  • 子进程仅复制父进程的页目录;

  • 子进程和父进程有各自独立的进程地址空间;

  • 共享物理地址空间;

  • 父进程不会被阻塞,如果fork()返回值是正数,则表示运行在父进程中且返回值是子进程pid;如果返回值是0,则表示运行在子进程中;如果返回值是-1,则表示创建失败;

使用fork()函数来创建子进程时,子进程和父进程有各自独立的进程地址空间,但是共享物理内存资源,当它们开始执行各自程序时,它们的进程地址空间开始分道扬镰,这得益于写时复制技术。

vfork()

  • 子进程不复制父进程的页目录和页表;
  • 父子进程共享进程地址空间;
  • 父进程会进入阻塞态等待子进程结束;

clone()

  • 创建用户态线程;
  • 带有很多参数,可以精确控制复制进程的各种行为(可以用来实现和fork、vfork完全相同的功能,也可创建线程)

1.进程的创建

我们所开发的程序在系统中都是以进程的形式运行的,进程在系统中执行着开发人员所提前设计好的逻辑,并使用CPU、内存等资源。那么进程是如何被创建出来的?如何做到从无到有的?本小节将深入到内核去梳理进程创建的流程,并对Linux-6.5及Linux-5.0两个版本的内核源码进行分析解读。

在用户态程序中,我们通常会使用fork()、vfork()、以及clone()来创建一个新的进程或新的线程,这些接口通过系统调用最终在内核中实现进程/线程创建;

在这里插入图片描述

1.1 系统调用进入内核

在源码中我们可以看到fork()、vfork()、clone()最终都是通过系统调用陷入内核,在内核中实现的创建工作,以下分别是Linux-6.5及Linux-5.0两个版本的相关源码:

Linux-5.0: /kernel/fork.c

asmlinkage long sys_clone(unsigned long, unsigned long, int __user *,
	       int __user *, unsigned long);
asmlinkage long sys_vfork(void);
asmlinkage long sys_fork(void);

Linux-5.0: /kernel/fork.c

/*fork系统调用入口*/
SYSCALL_DEFINE0(fork)
{
   
#ifdef CONFIG_MMU
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
	/* can not support in nommu mode */
	return -EINVAL;
#endif
}
/*vfork系统调用入口*/
SYSCALL_DEFINE0(vfork)
{
   
	return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
			0, NULL, NULL, 0);
}
/*clone系统调用入口*/
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 unsigned long, tls)
#endif
{
   
	return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}

Linux-6.5: /include/linux/syscalls.h

asmlinkage long sys_clone(unsigned long, unsigned long, int __user *,
	       int __user *, unsigned long);
asmlinkage long sys_vfork(void);	
asmlinkage long sys_fork(void);

Linux-6.5: /kernel/fork.c

/*fork系统调用入口*/
SYSCALL_DEFINE0(fork)
{
   
#ifdef CONFIG_MMU
	struct kernel_clone_args args = {
   
		.exit_signal = SIGCHLD,
	};
	return kernel_clone(&args);
#else
	/* can not support in nommu mode */
	return -EINVAL;
#endif
}
/*vfork系统调用入口*/
SYSCALL_DEFINE0(vfork)
{
   
	struct kernel_clone_args args = {
   
		.flags		= CLONE_VFORK | CLONE_VM,
		.exit_signal	= SIGCHLD,
	};
	return kernel_clone(&args);
}
/*clone系统调用入口*/
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值