Linux系统下init进程的前世今生

简介: Linux系统中的init进程(pid=1)是除了idle进程(pid=0,也就是init_task)之外另一个比较特殊的进程,它是Linux内核开始建立起进程概念时第一个通过kernel_thread产生的进程,其开始在内核态执行,然后通过一个系统调用,开始执行用户空间的/sbin...

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等程序:



  1. static noinline int init_post(void)
  2. {
  3.         ...
  4.         run_init_process("/sbin/init");
  5.         run_init_process("/etc/init");
  6.         run_init_process("/bin/init");
  7.         run_init_process("/bin/sh");
  8.         panic("No init found. Try passing init= option to kernel. "
  9.               "See Linux Documentation/init.txt for guidance.");
  10. }
run_init_process的核心调用就是kernel_execve,后者的实现代码是:

  1. int kernel_execve(const char *filename,
  2.                   const char *const argv[],
  3.                   const char *const envp[])
  4. {
  5.         long __res;
  6.         asm volatile ("int $0x80"
  7.         : "=a" (__res)
  8.         : "0" (__NR_execve), "b" (filename), "c" (argv), "d" (envp) : "memory");
  9.         return __res;
  10. }
里面是段内嵌的汇编代码,代码相对比较简单,核心代码是int 0x80,执行系统调用,系统调用号__NR_execve放在AX里,当然系统调用的返回值也是在AX中,要执行的用户空间应用程序路径名称保存在 BX中。int0x80,执行系统调用,系统调用号__NR_execve放在AX里,当然系统调用的返回值也是在AX中,要执行的用户空间应用程序路径名称保存在 BX中。int0x80的执行导致代码向__KERNEL_CS:system_call转移(具体过程可参考x86处理器中的特权级检查及Linux系统调用的实现一帖). 此处用bx,cx以及dx来保存filename, argv以及envp参数是有讲究的,它对应着struct pt_regs中寄存器在栈中的布局,因为接下来就会涉及从汇编到调用C函数过程,所以汇编程序在调用C之前,应该把要传递给C的参数在栈中准备好。

system_call是一段纯汇编代码:
  1. arch/x86/kernel/entry_32.s>
  2. ENTRY(system_call)
  3.         RING0_INT_FRAME # can't unwind into user space anyway
  4.         pushl_cfi %eax # save orig_eax
  5.         SAVE_ALL
  6.         GET_THREAD_INFO(%ebp)
  7.                                         # system call tracing in operation / emulation
  8.         testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
  9.         jnz syscall_trace_entry
  10.         cmpl $(nr_syscalls), %eax
  11.         jae syscall_badsys
  12. syscall_call:
  13.         call *sys_call_table(,%eax,4)
  14.         movl %eax,PT_EAX(%esp) # store the return value

  15. syscall_exit:
  16.         ...
  17. restore_nocheck:
  18.         RESTORE_REGS 4 # skip orig_eax/error_code
  19. irq_return:
  20.         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中对应的函数为:



  1. ENTRY(sys_call_table)
  2.         .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
  3.         .long sys_exit
  4.         .long ptregs_fork
  5.         .long sys_read
  6.         .long sys_write
  7.         .long sys_open /* 5 */
  8.         .long sys_close
  9.         ...
  10.         .long sys_unlink /* 10 */
  11.         .long ptregs_execve //__NR_execve
  12.         ...

ptregs_execve其实就是sys_execve函数:

  1. #define ptregs_execve sys_execve
而sys_execve函数的代码实现则是:

  1. /*
  2.  * sys_execve() executes a new program.
  3.  */
  4. long sys_execve(const char __user *name,
  5.                 const char __user *const __user *argv,
  6.                 const char __user *const __user *envp, struct pt_regs *regs)
  7. {
  8.         long error;
  9.         char *filename;

  10.         filename = getname(name);
  11.         error = PTR_ERR(filename);
  12.         if (IS_ERR(filename))
  13.                 return error;
  14.         error = do_execve(filename, argv, envp, regs);

  15. #ifdef CONFIG_X86_32
  16.         if (error == 0) {
  17.                 /* Make sure we don't return using sysenter.. */
  18.                 set_thread_flag(TIF_IRET);
  19.         }
  20. #endif

  21.         putname(filename);
  22.         return error;
  23. }
注意这里的参数传递机制!其中的核心调用是do_execve,后者调用do_execve_common来干执行一个新程序的活,在我们这个例子中要执 行的新程序来自/sbin/init,如果用file命令看一下会发现它其实是个ELF格式的动态链接库,而不是那种普通的可执行文件,所以 do_execve_common会负责打开、解析这个文件并找到其可执行入口点,这个过程相当繁琐,我们不妨直接看那些跟我们问题密切相关的代 码,do_execve_common会调用search_binary_handler去查找所谓的binary formats handler,ELF显然是最常见的一种格式:


  1. int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
  2. {
  3.        ...
  4.        for (try=0; try2; try++) {
  5.                 read_lock(&binfmt_lock);
  6.                 list_for_each_entry(fmt, &formats, lh) {
  7.                         int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;
  8.                         ...
  9.                         retval = fn(bprm, regs);
  10.                         ...
  11.                }
  12.                ...
  13.        }
  14. }
代码中针对ELF格式的 fmt->load_binary即为load_elf_binary, 所以fn=load_elf_binary, 后续对fn的调用即是调用load_elf_binary,这是个非常长的函数,直到其最后,我们才找到所需要的答案:


  1. static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
  2. {
  3.         ...
  4.         start_thread(regs, elf_entry, bprm->p);
  5.         ...
  6. }
上述代码中的elf_entry即为/sbin/init中的执行入口点, bprm->p为应用程序新栈(应该已经在用户空间了),start_thread的实现为:


  1. void
  2. start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
  3. {
  4.         set_user_gs(regs, 0);
  5.         regs->fs = 0;
  6.         regs->ds = __USER_DS;
  7.         regs->es = __USER_DS;
  8.         regs->ss = __USER_DS;
  9.         regs->cs = __USER_CS;
  10.         regs->ip = new_ip;
  11.         regs->sp = new_sp;
  12.         /*
  13.          * Free the old FP and other extended state
  14.          */
  15.         free_thread_xstate(current);
  16. }
在这里,我们看到了__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的调用:
  1. run_init_process("/sbin/init");
  2. run_init_process("/etc/init");
  3. run_init_process("/bin/init");
  4. run_init_process("/bin/sh");
但是只要比如/sbin/init被成功调用,run_init_process中的kernel_execve函数将无法返回,因为它执行int 0x80C4runinitprocess4panic.int0x80压入栈中的cs和ip的start_thread函数之后不会再有其他额外的代码导致整个调用链的失败,否则代码将执行非预期的指令,内核进入不稳定状态。

最后,我们来验证一下,所谓眼见为实,耳听为虚。再者,如果验证达到预期,也是很鼓舞人好奇心的极佳方法。验证的方法我打算采用“Linux设备驱动模型中的热插拔机制及实验” 中的路线,通过call_usermodehelper来做,因为它和kernel_execve本质上都是一样的。我们自己写个应用程序,在这个应用程序里读取cs寄存器的值,程序很简单:


  1. #include stdio.h>
  2. #include fcntl.h>
  3. #include unistd.h>
  4. #include syslog.h>

  5. int main()
  6. {
  7.     unsigned short ucs;
  8.     asm(
  9.         "movw %%cs, %0\n"
  10.         :"=r"(ucs)
  11.         ::"memory");
  12.     syslog(LOG_INFO, "ucs = 0x%x\n", ucs);
  13.     return 0;
  14. }
然后把这个程序打到/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)来达到特权级的更替。

目录
打赏
0
0
0
0
127
分享
相关文章
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
Linux系统资源管理:多角度查看内存使用情况。
要知道,透过内存管理的窗口,我们可以洞察到Linux系统运行的真实身姿,如同解剖学家透过微观镜,洞察生命的奥秘。记住,不要惧怕那些高深的命令和参数,他们只是你掌握系统"魔法棒"的钥匙,熟练掌握后,你就可以骄傲地说:Linux,我来了!
67 27
|
8天前
|
Linux系统ext4磁盘扩容实践指南
这个过程就像是给你的房子建一个新的储物间。你需要先找到空地(创建新的分区),然后建造储物间(格式化为ext4文件系统),最后将储物间添加到你的房子中(将新的分区添加到文件系统中)。完成这些步骤后,你就有了一个更大的储物空间。
57 10
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
33 4
|
1月前
|
Linux系统之whereis命令的基本使用
Linux系统之whereis命令的基本使用
103 24
Linux系统之whereis命令的基本使用
基于进程热点分析与系统资源优化的智能运维实践
智能服务器管理平台提供直观的可视化界面,助力高效操作系统管理。核心功能包括运维监控、智能助手和扩展插件管理,支持系统健康监控、故障诊断等,确保集群稳定运行。首次使用需激活服务并安装管控组件。平台还提供进程热点追踪、性能观测与优化建议,帮助开发人员快速识别和解决性能瓶颈。定期分析和多维度监控可提前预警潜在问题,保障系统长期稳定运行。
69 17
|
20天前
|
Linux系统中如何查看CPU信息
本文介绍了查看CPU核心信息的方法,包括使用`lscpu`命令和读取`/proc/cpuinfo`文件。`lscpu`能快速提供逻辑CPU数量、物理核心数、插槽数等基本信息;而`/proc/cpuinfo`则包含更详细的配置数据,如核心ID和处理器编号。此外,还介绍了如何通过`lscpu`和`dmidecode`命令获取CPU型号、制造商及序列号,并解释了CPU频率与缓存大小的相关信息。最后,详细解析了`lscpu`命令输出的各项参数含义,帮助用户更好地理解CPU的具体配置。
59 8
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
88 34
深度体验阿里云系统控制台:SysOM 让 Linux 服务器监控变得如此简单
作为一名经历过无数个凌晨三点被服务器报警电话惊醒的运维工程师,我对监控工具有着近乎苛刻的要求。记得去年那次大型活动,我们的主站流量暴增,服务器内存莫名其妙地飙升到90%以上,却找不到原因。如果当时有一款像阿里云 SysOM 这样直观的监控工具,也许我就不用熬通宵排查问题了。今天,我想分享一下我使用 SysOM 的亲身体验,特别是它那令人印象深刻的内存诊断功能。
|
29天前
|
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
41 7