Linux内核进程创建流程

news2024/11/24 22:39:05
本文代码基于Linux5.10
内容主要参考《Linux内核深度解析》余华兵

当Linux内核要创建一个新进程时, 流程大致如下

ret = fork();
if (ret == 0) {
    /* 子进程装载程序 */
    ret = execve(filename, argv, envp);
} else if (ret > 0) {
    /* 父进程 */
}

大致可以分为创建新进程和装载程序这两个过程。

创建新进程

Linux中创建新进程有两个系统调用, 分别是clone和fork, 其定义如下:

kernel/fork.c
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
}
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
		int, stack_size,
		int __user *, parent_tidptr,
		int __user *, child_tidptr,
		unsigned long, tls) {
  struct kernel_clone_args args = {
		.flags		= (lower_32_bits(clone_flags) & ~CSIGNAL),
		.pidfd		= parent_tidptr,
		.child_tid	= child_tidptr,
		.parent_tid	= parent_tidptr,
		.exit_signal	= (lower_32_bits(clone_flags) & CSIGNAL),
		.stack		= newsp,
		.tls		= tls,
	};

	return kernel_clone(&args);
}

可以理解为fork是clone的简化版本, clone可以更精确的控制创建进程的行为,我们在创建线程时,就是使用的clone(没错, 在Linux里面, 线程实际上也是进程)。

clone 和 fork 都会调用kernel_clone 这个函数去创建进程,只不过两者传递的参数不同。

Linux 目前通过kernel_clone_args 这个数据结构来传递参数。

include/linux/sched/task.h
struct kernel_clone_args {
	u64 flags;
	int __user *pidfd;
	int __user *child_tid;
	int __user *parent_tid;
	int exit_signal;
	unsigned long stack;
	unsigned long stack_size;
	unsigned long tls;
	pid_t *set_tid;
	/* Number of elements in *set_tid */
	size_t set_tid_size;
	int cgroup;
	struct cgroup *cgrp;
	struct css_set *cset;
};

flags : clone 标志。

stack : 只在创建线程时有意义, 用来指定线程的用户栈的地址

stack_size:只在创建线程时有意义, 用来指定线程的用户栈的大小

创建新进程的流程大致如下:

  1. 调用函数copy_process 创建新进程
  2. 调用函数wake_up_new_task 唤醒新进程。

copy process

copy process的流程如下:

1.检查标志是否合法。

kernel/fork.c
        /*
	 * Don't allow sharing the root directory with processes in a different
	 * namespace
	 */
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);

	if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
		return ERR_PTR(-EINVAL);
	.....

2.dup_task_struct。 已当前进程为模板, 创建task_struct数据结构

这里面会分配task_struct 的数据结构, 并分配内核栈。

内核栈也是一个slab。

kernel/fork.c
static unsigned long *alloc_thread_stack_node(struct task_struct *tsk,
						  int node)
{
	unsigned long *stack;
	stack = kmem_cache_alloc_node(thread_stack_cache, THREADINFO_GFP, node);
	stack = kasan_reset_tag(stack);
	tsk->stack = stack;
	return stack;
}
void thread_stack_cache_init(void)
{
	thread_stack_cache = kmem_cache_create_usercopy("thread_stack",
					THREAD_SIZE, THREAD_SIZE, 0, 0,
					THREAD_SIZE, NULL);
	BUG_ON(thread_stack_cache == NULL);
}

3.检查用户的进程数量限制

kernel/fork.c
	if (atomic_read(&p->real_cred->user->processes) >=
			task_rlimit(p, RLIMIT_NPROC)) {
		if (p->real_cred->user != INIT_USER &&
		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
			goto bad_fork_free;
	}

在用户空间, 可以通过ulimit -u 来设置用户最大可以创建的进程数量。

4.copy_creds

调用copy_cread 复制或者共享证书, 如果新进程和当前进程属于同一个线程组, 那么他们共享证书。

5. 检查线程数量限制

kernel/fork.c
	if (data_race(nr_threads >= max_threads))
		goto bad_fork_cleanup_count;nr_threads 会在每次创建进程/线程后+1

6.sched_fork

设置调度器相关的参数

7.复制或者共享资源

这里会复制虚拟内存,文件, 文件系统数据, 信号处理数据等各种资源。 这里重点介绍一下copy_thread 这个流程, 这里会复制进程的各种寄存器。

arch/arm64/kernel/process.c
int copy_thread(unsigned long clone_flags, unsigned long stack_start,
		unsigned long stk_sz, struct task_struct *p, unsigned long tls)
{
	struct pt_regs *childregs = task_pt_regs(p);

	memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));   /*        1         */

	/*
	 * In case p was allocated the same task_struct pointer as some
	 * other recently-exited task, make sure p is disassociated from
	 * any cpu that may have run that now-exited task recently.
	 * Otherwise we could erroneously skip reloading the FPSIMD
	 * registers for p.
	 */
	fpsimd_flush_task_state(p);

	ptrauth_thread_init_kernel(p);

	if (likely(!(p->flags & PF_KTHREAD))) {            /*        2         */         
		*childregs = *current_pt_regs();
		childregs->regs[0] = 0;													/*        3         */

		/*
		 * Read the current TLS pointer from tpidr_el0 as it may be
		 * out-of-sync with the saved value.
		 */
		*task_user_tls(p) = read_sysreg(tpidr_el0);

		if (stack_start) {                         /*        4         */
			if (is_compat_thread(task_thread_info(p)))
				childregs->compat_sp = stack_start;
			else
				childregs->sp = stack_start;
		}

		/*
		 * If a TLS pointer was passed to clone, use it for the new
		 * thread.
		 */
		if (clone_flags & CLONE_SETTLS)
			p->thread.uw.tp_value = tls;
	} else {                /*        5         */
		/*  
		 * A kthread has no context to ERET to, so ensure any buggy
		 * ERET is treated as an illegal exception return.
		 *
		 * When a user task is created from a kthread, childregs will
		 * be initialized by start_thread() or start_compat_thread().
		 */
		memset(childregs, 0, sizeof(struct pt_regs));
		childregs->pstate = PSR_MODE_EL1h | PSR_IL_BIT;

		p->thread.cpu_context.x19 = stack_start;
		p->thread.cpu_context.x20 = stk_sz;
	}
	p->thread.cpu_context.pc = (unsigned long)ret_from_fork; /*        6         */
	p->thread.cpu_context.sp = (unsigned long)childregs;

	ptrace_hw_copy_thread(p);

	return 0;
}

用户态相关的运行环境缓存在pt_regs 中, 内核态保存在thread结构体中。

(1) 获取pt_regs, 并初始化thread 结构体

(2) 对于用户进程的处理

(3) 设置返回值为0。(子进程fork返回0就是在这里设置)

(4) 设置线程的用户栈

(5) 对于内核进程的处理, 这里X19存储线程函数的地址,X20存放线程函数的参数

(6) 设置内核态的PC和SP值, 在发生进程切换时, 会切到原因的地方去

wake up new task

在新进程创建之后,会尝试去唤醒它,让它尽快得到执行, 其流程大致如下:

新进程第一次运行

前文说到,copy_thread是会把新进程的PC设置为ret_from_fork。

arch/arm64/kernel/entry.S
/*
 * This is how we return from a fork.
 */
SYM_CODE_START(ret_from_fork)
	bl	schedule_tail
	cbz	x19, 1f				// not a kernel thread
	mov	x0, x20
	blr	x19
1:	get_current_task tsk
	b	ret_to_user
SYM_CODE_END(ret_from_fork)

在ret_from_fork中, 首先进行调度切换的清理工作(schedule_tail)。 如果是用户进程,调用ret_to_user返回用户空间, 如果是内核进程,X19存储线程函数的地址,X20存放线程函数的参数, 这里会跳转到x19所存储的函数地址执行。

装载程序

一般来说, 用户层会调用execve或者execveat 执行某个具体的程序。

int execve(const char *filename, char *const argv[ ], char *const envp[ ]);

用户程序一般是一个elf文件, 内核会按照elf文件的格式去解析它, 并设置PC到对应的entry。这部分内容不在此详细说明。

实例: init 进程的创建和运行

init 是kernel运行的第一个进程, 我们来看看它是怎么创建和运行起来。

rest_init中,会调用kernel_thread 创建init进程

init/main.c
noinline void __ref rest_init(void)
{
	.....
	pid = kernel_thread(kernel_init, NULL, CLONE_FS);
	.....
}

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
	struct kernel_clone_args args = {
		.flags		= ((lower_32_bits(flags) | CLONE_VM |
				    CLONE_UNTRACED) & ~CSIGNAL),
		.exit_signal	= (lower_32_bits(flags) & CSIGNAL),
		.stack		= (unsigned long)fn,
		.stack_size	= (unsigned long)arg,
	};

	return kernel_clone(&args);
}

可以看到kernel_thread其实也是调用kernel_clone创建线程,其中stack被设置成了入口函数,stack_size被设置成了参数。

在kernel_init中, 会尝试装载init进程。

init/main.c
static int __ref kernel_init(void *unused)
{
.....
if (ramdisk_execute_command) {
       ret = run_init_process(ramdisk_execute_command);
       if (!ret)
         return 0;
       pr_err("Failed to execute %s (error %d)\n",
              ramdisk_execute_command, ret);
    }
....
}

装载完成之后, 就会调转到用户态的init进程执行了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/616716.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

pagehelper分页插件(SpringBoot,Mybatis整合前后端分析)

前言:在学习项目的过程中遇到了数据分页的功能,单纯的js前端不能处理大的数据量,需要后端整理好数据发送给前端,那么使用分页插件无疑是个好选择. 目录 pagehelper依赖 接口方法mapper Mybatis Service ServiceImpl PageResu…

[IJCAI 2022] 基于个性化掩码的实用安全联合推荐

Practical and Secure Federated Recommendation with Personalized Mask | SpringerLink 摘要 联合推荐解决了推荐系统的数据筒仓和隐私问题。目前的联合推荐系统主要利用密码学或混淆方法来保护原始评分不被泄露。然而,前者带来了额外的通信和计算成本&#xff0…

day 49 :121. 买卖股票的最佳时机;122. 买卖股票的最佳时机 II;123. 买卖股票的最佳时机 III

买卖股票 121. 买卖股票的最佳时机:一次买入卖出1. 贪心算法2. 动态规划1. dp数组以及下标名义2. 递归公式3. dp数组如何初始化4. 代码 122. 买卖股票的最佳时机 II:可以多次买入卖出2. 动态规划1. dp数组以及下标名义2. 递归公式3. dp数组如何初始化4. 代码 123. 买…

Linux_进程

目录 一.进程概念与子进程 1.进程基本概念 2.通过系统调用创建子进程-fork 二.进程状态 1、一般进程状态 2、Linux操作系统的进程状态 三.环境变量 1.概念 2.环境变量组织与获取 3.配置文件 4.环境变量的全局属性​编辑 5.命令行参数 四.进程优先级 1.查看系统进…

Linux文件系统-磁盘划分

一、磁盘使用 windows系统中: 1、分区 2、格式化 3、自动装载 4、使用 Linux系统中:1、分区 2、格式化 3、手动挂载 (挂载到/etc/fstab实现开机自启) 4、使用 Linux系统中磁盘使用: 1、分区操作…

rust:cargo 和rustc 以及一点 小技巧

在正式学习 Rust 语言以前,我们需要先学会怎样输出一段文字到命令行,这几乎是学习每一门语言之前必备的技能,因为输出到命令行几乎是语言学习阶段程序表达结果的唯一方式。 在之前的 Hello, World 程序中大概已经告诉了大家输出字符串的方式…

system V共享内存

一、前言 共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递将不再涉及到内核,换句话说,进程将不再通过执行进入系统内核的系统调用来传递彼此的数据。 但其实比它好用的进程间通信还有很多种&…

Android HTTP请求方式:HttpClient

1.HttpClient使用流程 基本流程: 2.HttpClient使用示例 1)使用HttpClient发送GET请求 直接贴下简单的发送Get请求的代码: public class MainActivity extends Activity implements OnClickListener { private Button btnGet; private WebV…

什么是OSPF被动接口?如何配置?华为、思科、瞻博网络三厂商命令来了

OSPF(开放最短路径优先)是一种常用的动态路由协议,用于在大型网络中实现路由选择。在OSPF中,被动接口是一种特殊类型的接口,它被用来监测网络中的邻居关系,并接收来自邻居发送的Hello消息。被动接口不主动发…

华为OD机试之在字符串中找出连续最长的数字串(含“+-”号)(Java源码)

在字符串中找出连续最长的数字串(含“”号) 输入描述 请在一个字符串中找出连续最长的数字串,并返回这个数字串。 如果存在长度相同的连续数字串,返回最后一个。 如果没有符合条件的字符串,返回空字符串””。 注意: 数字串可以由…

Ansible进阶2——角色管理

文章目录 一、角色1.1 获取角色方式1.2 角色结构1.3 定义变量和默认变量1.4 使用方法1.5 控制playbook中的任务执行流程 二、红帽企业Linux系统角色2.1 常见系统角色2.2 使用系统时间同步角色 三、自定义角色3.1 创建角色目录结构3.2 编写角色内容3.3 编写总结 四、ansible gal…

【C++】内存管理的基本操作,new与delete的实现原理以及operator new与operator delete函数

文章目录 前言一、new,delete操作内置类型二、new/delete操纵自定义类型3. operator new与operator delete函数4. new/delete实现原理4.malloc/free和new/delete的区别 前言 程序中内存的划分: 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向…

高考必胜,归来仍是少年!

高考必胜,归来仍是少年! 这是小索奇专门为高考生写的文章高考生 我以前给大家弄过一些免费的付费资料,现在看到后台很多伙伴们都在寻找资料,一些没有充分准备的小伙伴此刻一定很匆忙吧! 我想对大家说: 高…

基于 FFMPEG 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)

系列文章目录 基于 FFMPEG 的跨平台视频播放器简明教程(一):FFMPEG Conan 环境集成 文章目录 系列文章目录前言基础知识视频,你所看到的!音频 - 你所听到的声音编解码器 - 压缩数据容器 - 存放音频和视频的地方 解封…

vue3.0与vue2.0的区别简记(基于官方文档)

vue3.0与vue2.0的区别简记(基于官方文档) 基于vue3.0和vue2.0官方文档简单记录vue3.0版本和2.0版本的区别。 一直没有看文档的习惯(就是不爱学习,现在吃了没文化的亏),遇到问题才去补充点食粮&#xff0c…

祝2023高考考生高考顺利!金榜题名

前言:光阴似箭,岁月如梭。明天就是全国每年一次的高考了,我也即将结束我的大一生活成为一名大二的小学长啦嘿嘿。而我今天呢主要是想祝马上要高考的学弟学妹们高考顺利,金榜题名,并且借此机会顺便讲讲我的高考前后的故…

解决python通过pip离线安装flask,numpy报错解决(centos)

1. 离线安装Python https://www.python.org/ftp/python/3.7.1/Python-3.7.1.tgz 解压,编译,安装 tar xzvf Python-3.7.1.tgz ./configuremakemake install 离线环境下如果系统不是完整版安装编译会报错,需要解决依赖问题,如下&am…

5 种常见的 Linux 打包类型:tar、gzip、bzip2、zip 、 7z

在 Linux 系统中,打包和压缩文件是常见的操作。不同的打包类型适用于不同的用途和需求。本文将详细介绍 5 种常见的 Linux 打包类型,包括tar、gzip、bzip2、zip 和 7z,以及它们的特点、使用方法和适用场景。 1. tar tar(tape arc…

音悦台项目测试报告

文章目录 项目背景项目功能测试计划与设计功能测试自动化测试 测试结果功能测试结果UI自动化测试结果 项目背景 现如今人们的生活压力大,容易使人疲惫,为了使得人们在闲暇之余可以听音乐放松,为此设计出一款轻量的听音乐网站,快速…

centos安装部署Kubernetes(k8s)步骤使用kubeadm方式

文章目录 1、修改系统配置2、安装docker应用3. 拉取docker镜像4、cri-dockerd安装5、安装kubeadm和kubelet6、配置flannel网络插件7、Node节点加入集群操作 机器地址: 192.168.0.35 k8s-master 192.168.0.39 k8s-node1 192.168.0.116 k8s-node2 1、修改系统配置 修…