linux init进程原理,Linux 系统下 init 进程的前世今生
原标题:Linux 系统下 init 进程的前世今生
Linux系统中的 init 进程 (pid=1) 是除了 idle 进程 (pid=0,也就是 init_task) 之外另一个比较特殊的进程,它是 Linux 内核开始建立起进程概念时第一个通过 kernel_thread 产生的进程,其开始在内核态执行,然后通过一个系统调用,开始执行用户空间的 / sbin/init 程序,期间 Linux 内核也经历了从内核态到用户态的特权级转变,/sbin/init 极有可能产生出了 shell,然后所有的用户进程都有该进程派生出来 (目前尚未阅读过 / sbin/init 的源码)...
目前我们至少知道在内核空间执行用户空间的一段应用程序有两种方法:
1. call_usermodehelper
2. kernel_execve
它们最终都通过 int $0x80 在内核空间发起一个系统调用来完成,这个过程我在《深入 Linux 设备驱动程序内核机制》第 9 章有过详细的描述,对它的讨论最终结束在 sys_execve 函数那里,后者被用来执行一个新的程序。现在一个有趣的问题是,在内核空间发起的系统调用,最终通过 sys_execve 来执行用户 空间的一个程序,比如 / sbin/myhotplug,那么该应用程序执行时是在内核态呢还是用户态呢?直觉上肯定是用户态,不过因为 cpu 在执行 sys_execve 时 cs 寄存器还是__KERNEL_CS,如果前面我们的猜测是真的话,必然会有个 cs 寄存器的值从__KERNEL_CS 到 __USER_CS 的转变过程,这个过程是如何发生的呢?下面我以 kernel_execve 为例,来具体讨论一下其间所发生的一些有趣的事情。
start_kernel 在其最后一个函数 rest_init 的调用中,会通过 kernel_thread 来生成一个内核进程,后者则会在新进程环境下调 用 kernel_init 函数,kernel_init 一个让人感兴趣的地方在于它会调用 run_init_process 来执行根文件系统下的 /sbin/init 等程序:
static noinline int init_post(void)
...
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel."
"See Linux Documentation/init.txt for guidance.");
}
run_init_process 的核心调用就是 kernel_execve,后者的实现代码是:
int kernel_execve(const char *filename,
const char *const argv[],
const char *const envp[])
{
long __res;
asm volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_execve), "b" (filename), "c" (argv), "d" (envp) : "memory");
return __res;
}
里面是段内嵌的汇编代码,代码相对比较简单,核心代码是 int $0x80,执行系统调用,系统调用号__NR_execve 放在 AX 里,当然系统调用的返回值也是在 AX 中,要执行的用户空间应用程序路径名称保存在 BX 中。int $0x80 的执行导致代码向__KERNEL_CS:system_call 转移 (具体过程可参考 x86 处理器中的特权级检查及 Linux 系统调用的实现一帖). 此处用 bx,cx 以及 dx 来保存 filename, argv 以及 envp 参数是有讲究的,它对应着 struct pt_regs 中寄存器在栈中的布局,因为接下来就会涉及从汇编到调用 C 函数过程,所以汇编程序在调用 C 之前,应该把要传递给 C 的参数在栈中准备好。
system_call 是一段纯汇编代码:
ENTRY(system_call)
RING0_INT_FRAME # can't unwind into user space anyway
pushl_cfi %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
movl %eax,PT_EAX(%esp) # store the return value
syscall_exit:
...
restore_nocheck:
RESTORE_REGS 4 # skip orig_eax/error_code
irq_return:
INTERRUPT_RETURN #iret instruction for x86_32
system_call 首先会为后续的 C 函数的调用在当前堆栈中建立参数传递的环境 (x86_64 的实现要相对复杂一点,它会将系统调用切换到内核栈 movq PER_CPU_VAR(kernel_stack),%rsp),尤其是接下来对 C 函数 sys_execve 调用中的 struct pt_regs *regs 参数,我在上面代码中同时列出了系统调用之后的后续操作 syscall_exit,从代码中可以看到系统调用 int $0x80 最终通过 iret 指令返回,而后者会从当前栈中弹出 cs 与 ip,然后跳转到 cs:ip 处执行代码。正常情况下,x86 架构上的 int n指 令会将其下条指令的 cs:ip 压入堆栈,所以当通过 iret 指令返回时,原来的代码将从 int n 的下条指令继续执行,不过如果我们能在后续的 C 代码中改变 regs->cs 与 regs->ip(也就是 int n执行时压入栈中的 cs 与 ip),那么就可以控制下一步代码执行的走向,而 sys_execve 函数的调用链正好利用了这一点,接下来我们很快就会看到。SAVE_ALL 宏的最后为将 ds, es, fs 都设置为__USER_DS,但是此时 cs 还是__KERNEL_CS.
核心的调用发生在 call *sys_call_table(,%eax,4) 这条指令上,sys_call_table 是个系统调用表,本质上就是一个函数指针数组,我们这里的系 统调用号是__NR_execve=11, 所以在 sys_call_table 中对应的函数为:
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
...
.long sys_unlink /* 10 */
.long ptregs_execve //__NR_execve
...
ptregs_execve 其实就是 sys_execve 函数:
#define ptregs_execve sys_execve
#define ptregs_execve sys_execve
而 sys_execve 函数的代码实现则是:
/*
* sys_execve() executes a new program.
*/
long sys_execve(const char __user *name,
const char __user *const __user *argv,
const char __user *const __user *envp, struct pt_regs *regs)
{
long error;
char *filename;
filename = getname(name);
error = PTR_ERR(filename);
if (IS_ERR(filename))
return error;
error = do_execve(filename, argv, envp, regs);
#ifdef CONFIG_X86_32
if (error == 0) {
/* Make sure we don't return using sysenter.. */
set_thread_flag(TIF_IRET);
}
#endif
putname(filename);
return error;
}
注意这里的参数传递机制!其中的核心调用是 do_execve, 后者调用 do_execve_common 来干执行一个新程序的活,在我们这个例子中要执 行的新程序来自 / sbin/init,如果用 file 命令看一下会发现它其实是个 ELF 格式的动态链接库,而不是那种普通的可执行文件,所以 do_execve_common 会负责打开、解析这个文件并找到其可执行入口点,这个过程相当繁琐,我们不妨直接看那些跟我们问题密切相关的代 码,do_execve_common 会调用 search_binary_handler 去查找所谓的 binary formats handler,ELF 显然是最常见的一种格式:
int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{
...
for (try=0; try<2; try++) {
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
...
retval = fn(bprm, regs);
...
}
...
}
}
代码中针对 ELF 格式的 fmt->load_binary 即为 load_elf_binary, 所以 fn=load_elf_binary, 后续对 fn 的调用即是调用 load_elf_binary,这是个非常长的函数,直到其最后,我们才找到所需要的答案:
static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
{
...
start_thread(regs, elf_entry, bprm->p);
...
}
上述代码中的 elf_entry 即为 / sbin/init 中的执行入口点, bprm->p 为应用程序新栈 (应该已经在用户空间了),start_thread 的实现为:
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
set_user_gs(regs, 0);
regs->fs = 0;
regs->ds = __USER_DS;
regs->es = __USER_DS;
regs->ss = __USER_DS;
regs->cs = __USER_CS;
regs->ip = new_ip;
regs->sp = new_sp;
/*
* Free the old FP and other extended state
*/
free_thread_xstate(current);
}
在这里,我们看到了__USER_CS 的身影,在 x86 64 位系统架构下,该值为 0x33. start_thread 函数最关键的地方在于修改了 regs->cs= __USER_CS, regs->ip= new_ip,其实就是人为地改变了系统调用 int $0x80 指令压入堆栈的下条指令的地址,这样当系统调用结束通过 iret 指令返回时,代码将从这里的__USER_CS:elf_entry 处开始执 行,也就是 / sbin/init 中的入口点。start_thread 的代码与 kernel_thread 非常神似,不过它不需要象 kernel_thread 那样在最后调用 do_fork 来产生一个 task_struct 实例出来了,因为目前只需要在当前进程上下文中执行代码,而不是创建一个新进程。关于 kernel_thread,我在本版曾有一篇帖子分析过,当时基于的是 ARM 架构。
所以我们看到,start_kernel 在最后调用 rest_init,而后者通过对 kernel_thread 的调用产生一个新进程 (pid=1),新进程在其 kernel_init()-->init_post() 调用链中将通过 run_init_process 来执行用户空间的 / sbin /init,run_init_process 的核心是个系统调用,当系统调用返回时代码将从 / sbin/init 的入口点处开始执行,所以虽然我们知道 post_init 中有如下几个 run_init_process 的调用:
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
但是只要比如 / sbin/init 被成功调用,run_init_process 中的 kernel_execve 函数将无法返回,因为它执行 int $0x80 时压入堆栈中回家的路径被后续的 C 函数调用链给改写了,这样 4 个 run_init_process 只会有一个有机会被成功执行,如果这 4 个函数都失败 了,那么内核将会 panic. 所以内核设计时必须确保用来改写 int $0x80 压入栈中的 cs 和 ip 的 start_thread 函数之后不会再有其他额外的代码导致整个调用链的失败,否则代码将执行非预期的指令,内核进入不稳定状态。
最后,我们来验证一下,所谓眼见为实,耳听为虚。再者,如果验证达到预期,也是很鼓舞人好奇心的极佳方法。验证的方法我打算采用 “Linux 设备驱动模型中的热插拔机制及实验” 中的路线,通过 call_usermodehelper 来做,因为它和 kernel_execve 本质上都是一样的。我们自己写个应用程序,在这个应用程序里读取 cs 寄存器的值,程序很简单:
#include
#include
#include
#include
int main()
{
unsigned short ucs;
asm(
"movw %%cs, %0n"
:"=r"(ucs)
::"memory");
syslog(LOG_INFO, "ucs = 0x%xn", ucs);
return 0;
}
然后把这个程序打到 / sys/kernel/uevent_help 上面 (参照 Linux 设备驱动模型中的热插拔机制及实验一文),之后我们往电脑里插个 U 盘,然后到 / var/log/syslog 文件里看输出 (在某些 distribution 上,syslog 的输出可能会到 / var/log/messages 中):
Mar 10 14:20:23 build-server main: ucs = 0x33
0x33 正好就是 x86 64 位系统 (我实验用的环境) 下的__USER_CS.
所以第一个内核进程 (pid=1) 通过执行用户空间程序,期间通过 cs 的转变 (从__KERNEL_CS 到__USER_CS) 来达到特权级的更替。
文章来源:CU技术社区
《深入JVM内核—原理、诊断与优化》由葛一鸣老师亲授!本课程为Java进阶课程,通过学习熟悉JVM的工作机制,了解Java虚拟机的工作原理,知道如何处理Java程序开发与运行中出现各种问题,故障诊断、以及调优!返回搜狐,查看更多
责任编辑:
linux init进程原理,Linux 系统下 init 进程的前世今生相关推荐
- Linux、Mac、windows 系统下 配置Python虚拟环境 virtualenvwrapper教程,workon切换环境, 非常简单方便
前言 开发及测试过程中每个环境都有各自的作用,避免破坏其他环境所以建议每个项目新建一个虚拟环境,经过对比最终我选择了virtualenvwrapper来作为我开发中虚拟环境管理工具,接下来一起来看一下 ...
- Supervisor-类unix系统下的进程控制工具
如果你的英文足够好,请看官网的文档:http://supervisord.org/introduction.html 简介: Supervisor 类unix系统下的进程控制工具. 特性: 1.配置简 ...
- 在Linux系统下实现进程,Linux进程学习(一)之Linux进程的基本知识和实现
最近一周学习了Linux 进程编程的知识,现对其总结如下. 在第一部分中我们先对进程的基本概念以及在Linux 中是如何来现实进程的进行介绍 Tiger-John说明 : 许多人在学习中只注重如何编程 ...
- linux java进程消失_Linux系统下的Java进程无故消失怎么办?
Linux系统步骤的一些Java项目总是无故的消失,原来是Java进程被关闭掉了.为什么会出现这种情况呢?有可能是被系统自动清除多余进程,或是其他程序关掉了Java项目,这个时候该怎么办呢? 解决方法 ...
- linux关闭wps画布进程,在Deepin系统下安装WPS后一直出现一个无响应的wpsoffice进程...
很多人在Deepin 20 Linux系统下安装WPS新版本后都出现一个无响应的wpsoffice进程,不过似乎不是共有的问题,有些人表示没有这个. 进一步说明 如果把WPS给卸载掉然后再重新安装,还 ...
- linux安装多版本php_Linux系统下为Nginx安装多版本PHP
我们在安装配置服务器LNPM环境时应该考虑到PHP多版本并存的问题,下面是实现Linux系统下为Nginx安装多版本PHP的实现方法 linux版本:64位CentOS 6.4 Nginx版本:ngi ...
- linux安装 mysql-5.7.25_Linux 系统下安装 mysql5.7.25(glibc版)
前言:经过一天半的折腾,终于把 mysql 5.7.25 版本安装上了 Amazon Linux AMI release 2017.09系统上,把能参考的博客几乎都看了一遍,终于发现这些细节问题,然而 ...
- linux mysql 测试工具_LINUX系统下MySQL 压力测试工具super smack
LINUX系统下MySQL 压力测试工具super smack 发布时间:2008-09-08 17:03:39 作者:佚名 我要评论 1. 源文件下载地址:http://vegan.net/ ...
- linux vnc 禁止复制,Linux_Linux系统下VNC SERVER的相关配置,安装: 复制代码代码如下: - phpStudy...
Linux系统下VNC SERVER的相关配置 安装: 复制代码代码如下: yum install tigervnc-server 配置: (1) 复制配置文件: 复制代码代码如下: ~]# cp / ...
最新文章
- 【POCO】POCO学习总结(三)——交叉编译
- limbo可以运行linux,这次真的了,安卓手机可以安装 Windows 10 了
- Elasticsearch介绍Kibana分词器增删改操作
- python实现图结构github_Github项目+代码:新型深度网络体系结构去除图像中的雨水痕迹...
- Jmeter提取响应结果中的json数据
- pingfangsc字体_2020-iOS GUI-字体规范
- vue点击input框出现弹窗_使用vue实现各类弹出框组件
- 放心!没人在意你使用的是命令式编程还是声明式编程
- git 编辑提交的技巧
- 前端代码异常日志收集与监控
- iBaits中,关于insert返回值的问题(注意!!!不必写resultClass= java.lang.Integer,方法的返回值就是int)
- 态调用Excel避免因为版本不同而使用程序无法编辑或调试
- 前端视频插件Video.js的基本使用
- 百度地图和谷歌地图经纬度互转
- android应用间相互调用
- 广东工业大学龙洞校区全国计算机,广东工业大学龙洞校区简介
- 雅虎财经api_雅虎! 发布音乐API
- 几何重数(geometric multiplicity)与代数重数 (algebraic multiplicity)
- 学习Python中turtle模块的基本用法(3:学习绘图示例源码)
- Cannot create resource output directory