sysvinit
Sysvinit 项目工具简介
项目背景介绍
init 进程是 Unix 和 Linux 系统中,用来产生其他所有进程的程序。init 的进程号 pid 是1,它是其他所有进程的祖先。
关于 init 进程的实现方法,历史上有过两种实现方案,BSD 风格和 SysV 风格。
BSD 风格比较简单,也就是通常在很多嵌入式Linux系统中经常可以看到的 /etc/rc 脚本方式,init 进程负责从这个启动脚本中读取一系列需要执行的程序,依次执行直到最后启动 getty (基于文本模式的终端)或者 X (基于图形界面的窗口系统)。
Sysv 相对复杂一些,它提出了一个关于 runlevel 运行级别的概念,为了实现启动系统到不同的运行级别,它引入了一个 /etc/inittab 的启动配置文件,通过这个配置文件指定的 initdefault 的值(从0-6或者S),可以引导系统分别进入到单用户模式,多用户模式(还可以分为有网络连接和无网络连接两种),多用户带图形界面方式 和用户自定义方式等,同时也可以引导系统到关机模式和重启模式。
大部分 Linux 的发行版都是采用和 System V init 相兼容的方式启动,为了配置 init 进程的这些运行级别和功能,sysvinit 的作者 Miquel van Smoorenburg 开发实现了一组软件包来完成这些功能。这个软件包就是我们所要研究的 sysvinit 项目,其中所包含的工具在下面我们要介绍的项目技术架构中会详细阐述。
下面我们列举一些有关 sysvinit 项目所需要用到的资源,以便了解和查看。
项目相关资源链接
开发成员
Petter Reinholdtsen
Roger Leigh
Dr. Werner Fink
其他类似软件包
事实上,从 sysvinit 产生之后,又出现了许多类似的项目,其主要功能都是来帮助内核最终完成启动用户应用程序。我们列举其中一些比较常见知名的项目,作为后继研究的参考。
Upstart
最早在 Ubuntu 6.10 中广泛采用的,基于事件机制 event-based 的,异步方式工作的 init 进程方案,完全可以兼容 sysvinit 的脚本。
现在也被很多其他操作系统发行版所采用,例如 Google Chrome OS,HP Web OS,中均采用了 upstart 来作为默认的init启动程序。SystemStarter
在 Mac OS X v10.4 之前所采用的 BSD 风格的 init 进程方案。launchd
在 Mac OS X v10.4 和之后所采用,它会启动 SystemStarter 来处理 rc.local 脚本。systemd
能够提供更好的服务依赖性的支持,允许系统启动期间能够并发完成更多工作,减小shell的开销。
systemd 在 Fedora 15 和 openSUSE 12.1 中所采用。
项目技术架构
Sysvinit 软件包是一组Linux工具集,包含控制启动,运行和关闭所有其他程序的工具。
具体包含有如下这些命令:
init
telinit (链接到 init)
sulogin
halt
poweroff (链接到 halt)
reboot (链接到 halt)
shutdown
last, lastb (链接到 last)
killall5
pidof (链接到 killall5)
mesg
wall
runlevel
utmpdump
这些命令多达13条,我们可以把它们按功能和用途进行归类。
核心命令 init/telinit
init 是所有命令中的核心,也是该项目最重要的输出成果,提供一个可以和 Kernel 进行对接的 init 程序,实现支持不同运行级别的启动方式。
telinit 是一个 init 程序的软链接,用来通知 init 进程切换运行级别。
关机命令 halt/shutdown/poweroff/reboot
这4条命令都是和系统关机有关,名字很接近。其中 shutdown 命令是这里面的核心命令,halt 命令是通过调用 shutdown 命令来完成功能的。
poweroff 和 reboot 这2条命令,都是指向 halt 命令的软链接。我们把它们可以统称为关机命令。
运行级别命令 sulogin/runlevel
sulogin 是为单用户运行级别提供的登录验证程序,用户运行这个命令就会以 root 方式进入单用户模式。
runlevel 是一条测试命令,检查当前的运行级别,和之前上一个运行级别,打印出来给用户以便了解。
进程/消息相关命令 mesg/wall/killall5/pidof
这里面的命令都是和进程/消息有关的。mesg 是给某个用户和终端发消息,wall 是给全部登录用户发消息。
killall5 是向所有进程发送 SIGKILL 消息,杀死进程。 pidof 是一个 killall5 的软链接,打印出对应输入进程名 progname 的进程号 pid。
日志相关命令 bootlogd/utmpdump
bootlogd 命令可以设置启动时候输出的信息到日志文件中,默认的日志文件是 /var/log/boot 。
utmpdump 命令可以实现以便于查看的格式输出 /var/run/utmp 文件日志内容。
文件系统相关命令 mountpoint/fstab-decode
mountpoint 命令用来检查某个目录是否是一个挂载点,也就是 mount 上来的目录。例如 / 和 /proc 一般都是,但 /etc /lib 一般不是。
fstab-decode 命令用来运行一条命令,在这个命令中可以加上一些参数,由 fstab-decode 来解析。
Sysvinit 项目概要分析
代码实现概要分析
我们目前所要分析的源码压缩包为 2.88 版本的,这个版本的发布时间是 26-Mar-2010。
源码目录结构
源文件代码量分析
src 目录下的源码文件共 17 个,全部代码量一共 9350 行,接近一万行。其中 init.c 是这里面代码量最大的,约有3000行。killall5.c 约有1100行。其他的源代码都在1000行以内。
Makefile 分析
93 init: LDLIBS +=
94 init: init.o init_utmp.o
95
96 halt: halt.o ifdown.o hddown.o utmp.o reboot.h
97
98 last: last.o oldutmp.h
99
100 mesg: mesg.o
101
102 mountpoint: mountpoint.o
103
104 utmpdump: utmpdump.o
105
106 runlevel: runlevel.o
107
108 sulogin: LDLIBS +=
109 sulogin: sulogin.o
110
111 wall: dowall.o wall.o
112
113 shutdown: dowall.o shutdown.o utmp.o reboot.h
114
115 bootlogd: LDLIBS += -lutil
116 bootlogd: bootlogd.o
以上是生成可执行文件的 Makefile 关键片段。从这里可以大致看出,每个可执行文件的生成,需要依赖于哪些目标文件,也就是由哪些源码文件生成的。例如 init 程序,是由 init.c init_utmp.c 这2个文件生成,如果我们要研究 init 程序的源码,就需要读懂这2个源码文件。
项目下载编译执行
wget下载源码包
tar解压源码包
修改 Makefile
增添11行的 CC=gcc,注释掉 13,14行有关 CFLAGS 的定义,否则编译会出很多的警告错误。
在80行处添加83行处的赋值,增加链接时 -lcrypt 选项
编译项目源码
$ cd sysvinit-2.88dsf/
$ make
gcc -D_GNU_SOURCE -DDEBUG -c -o mountpoint.o mountpoint.c
gcc mountpoint.o -o mountpoint
gcc -D_GNU_SOURCE -DDEBUG -c -o init.o init.c
gcc -D_GNU_SOURCE -DDEBUG -DINIT_MAIN -c -o init_utmp.o utmp.c
gcc init.o init_utmp.o -o init
gcc -D_GNU_SOURCE -DDEBUG -c -o halt.o halt.c
gcc -D_GNU_SOURCE -DDEBUG -c -o ifdown.o ifdown.c
gcc -D_GNU_SOURCE -DDEBUG -c -o hddown.o hddown.c
gcc -D_GNU_SOURCE -DDEBUG -c -o utmp.o utmp.c
gcc halt.o ifdown.o hddown.o utmp.o reboot.h -o halt
gcc -D_GNU_SOURCE -DDEBUG -c -o shutdown.o shutdown.c
gcc -D_GNU_SOURCE -DDEBUG -c -o dowall.o dowall.c
gcc shutdown.o dowall.o utmp.o reboot.h -o shutdown
gcc -D_GNU_SOURCE -DDEBUG -c -o runlevel.o runlevel.c
gcc runlevel.o -o runlevel
gcc -D_GNU_SOURCE -DDEBUG -c -o sulogin.o sulogin.c
gcc sulogin.o -lcrypt -o sulogin
gcc -D_GNU_SOURCE -DDEBUG -c -o bootlogd.o bootlogd.c
gcc bootlogd.o -lutil -o bootlogd
gcc -D_GNU_SOURCE -DDEBUG -c -o last.o last.c
gcc last.o oldutmp.h -o last
gcc -D_GNU_SOURCE -DDEBUG -c -o mesg.o mesg.c
gcc mesg.o -o mesg
gcc -D_GNU_SOURCE -DDEBUG -c -o utmpdump.o utmpdump.c
gcc utmpdump.o -o utmpdump
gcc -D_GNU_SOURCE -DDEBUG -c -o wall.o wall.c
gcc wall.o dowall.o -o wall
$
查看生成的可执行文件
工具安装使用流程
因为这些工具都是系统工具,执行 make install 安装之后,会覆盖掉原来系统的相关工具,因此这里所做的安装,是安装到了当前目录下一个叫 root 的目录下,并没有安装到 / 根目录下。特此说明。
在 Makefile 中,有一个目标 install ,其中有一个宏变量 ROOT 可以修改为 当前目录下的 root 目录,可以提前用 mkdir root 建好。
执行 make install 的时候,加上 ROOT=root 就可以了。
工具安装
$ mkdir root
$ make install ROOT=root
install -m 755 -d root/bin/ root/sbin/
install -m 755 -d root/usr/bin/
for i in mountpoint; do \
install -m 755 $i root/bin/ ; \
done
for i in init halt shutdown runlevel killall5 fstab-decode sulogin bootlogd; do \
install -m 755 $i root/sbin/ ; \
done
for i in last mesg utmpdump wall; do \
install -m 755 $i root/usr/bin/ ; \
done
install -m 755 -d root/etc/
install -m 755 initscript.sample root/etc/
ln -sf halt root/sbin/reboot
ln -sf halt root/sbin/poweroff
ln -sf init root/sbin/telinit
ln -sf /sbin/killall5 root/bin/pidof
if [ ! -f root/usr/bin/lastb ]; then \
ln -sf last root/usr/bin/lastb; \
fi
install -m 755 -d root/usr/include/
install -m 644 initreq.h root/usr/include/
install -m 755 -d root/usr/share/man/man1/
install -m 755 -d root/usr/share/man/man5/
install -m 755 -d root/usr/share/man/man8/
for i in last.1 lastb.1 mesg.1 utmpdump.1 mountpoint.1 wall.1; do \
install -m 644 ../man/$i root/usr/share/man/man1/; \
done
for i in initscript.5 inittab.5; do \
install -m 644 ../man/$i root/usr/share/man/man5/; \
done
for i in halt.8 init.8 killall5.8 pidof.8 poweroff.8 reboot.8 runlevel.8 shutdown.8 telinit.8 fstab-decode.8 sulogin.8 bootlogd.8; do \
install -m 644 ../man/$i root/usr/share/man/man8/; \
done
$ ls root/
bin sbin usr
$
查看安装目录
工具的安装目录主要包括 /sbin, /bin, /usr/bin 这3个地方,当然我们这次是属于模拟安装,所以都会安装在 root 目录下。
查看帮助文件
所有工具编译之后可执行文件都生成在 src 源码目录树下,同时,这些命名的帮助文件在 man 目录下。
通过使用 man 命令,加上 -l 参数,例如 man -l init.8 我们可以了解到这些命令的用法。
注意
我们这里没有直接使用例如 man init 这样的命令,而是改用 man -l init.8,这是因为前者是查看当前系统的帮助,而当前系统是 ubuntu 12.04 已经改用 upstart 作为 init 进程。后者才是针对 sysvinit 工具中的可执行文件配套的帮助信息。
下面我们针对这些命令的帮助信息,来给出每个命令的具体用法,在测试案例报告中,我们会详细说明每个命令如何使用。
init 命令
init 命令说明
init 进程是所有进程的父进程。它的主要任务就是从 /etc/inittab 文件中读取命令行,从而创建出一系列后继进程。
init 进程本身是被 Kernel 所启动,Kernel 将控制权交给它之后,用它来负责启动所有其他的进程。
inittab 文件中通常有关于登录接口的定义,就是在每个终端产生getty,使用户可以进行登录.
init 命令实现代码可以参考源文件
init.c
init_utmp.c
命令格式
/sbin/init [ -a ] [ -s ] [ -b ] [ -z xxx ] [ 0123456Ss ]
运行级别
运行级别是Linux操作系统的一个软件配置,用它来决定启动哪些程序集来运行。
系统启动时,可以根据 /etc/inittab 文件的配置,进入不同的运行级别。
每个运行级别可以设置启动不同的程序。
启动的每个程序都是init的进程的子进程,运行级别有8个,分别是 0-6,S或s。
运行级别0,1和6是系统保留的。
运行级别0用来关闭系统,
运行级别1先关闭所有用户进程和服务,然后进入单用户模式。
运行级别6用来重启系统。
运行级别S和s,会直接进入到单用户模式。
这种模式下不再需要 /etc/inittab 文件。
/sbin/sulogin 会在 /dev/console 上 被启动。
运行级别S和s的功能是相同的。
启动过程
在kernel启动的最后阶段,会调用init。init会查找/etc/inittab文件内容,进入指定的运行级别。
其中 initdefault 代表着系统默认要进入的运行级别,如果用户指定了,就会进入到 initdefault 代表的那个运行级别。
如果用户没有指定,则系统启动时,会通过 console 来要求用户输入一个运行级别。
当启动一个新进程时,init会先检查/etc/initscript文件是否存在。如果存在,则使用这个脚本来启动那个进程。
选项
-s, S, single
进入单用户模式.1-5
启动进入的运行级别.-b, emergency
直接进入单用户shell,不运行任何其他的启动脚本。-a, auto
如果指定该参数,init 会将 AUTOBOOT 环境变量设置为 yes。-z xxx
-z后面的参数将被忽略。可以使用这种方法将命令行加长一点,这样可以增加在堆栈中占用的空间。init 0
这条命令也可以用来关机。
/etc/inittab文件范例
id
inittab 文档中条目的唯一标识, 限于1-4 个字符。
runlevels
列出发生指定动作的运行级,可以是单个的数字,也可以是连续的多个数字,例如2345表示在多个运行级别下都需要执行。
action
描述要发生的动作,常用的有 respawn, wait, boot, once, bootwait, off, initdefault, ctrlaltdel, sysinit 等。具体含义如下:
process
要执行的程序或者脚本的名称,常见的有 getty,/etc/init.d/rcS, /etc/rc.d/rc.sysinit, /etc/rc.d/rc, /bin/sh, /bin/umount 等。
shutdown 命令
shutdown 命令说明
shutdown 以一种安全的方式终止系统,所有正在登录的用户都会收到系统将要终止的通知,并且不准新的登录。
shutdown 命令实现代码可以参考源文件
shutdown.c
dowall.c
utmp.c
reboot.h
命令格式
/sbin/shutdown [-akrhPHfFnc] [-t sec] time [warning message]
参数选项
-h
将系统关机,在某种程度上功能与halt命令相当。-k
只是送出信息给所有用户,但并不会真正关机。-n
不调用init程序关机,而是由shutdown自己进行(一般关机程序是由shutdown调用init来实现关机动作),使用此参数将加快关机速度,但是不建议用户使用此种关机方式。-r
shutdown之后重新启动系统。-f <秒数>
送出警告信息和关机信号之间要延迟多少秒。警告信息将提醒用户保存当前进行的工作
halt 命令
halt 命令说明
halt 用来停止系统。正常情况下等效于 shutdown 加上 -h 参数(当前系统运行级别是 0 时除外)。它将告诉内核去中止系统,并在系统正在关闭的过程中将日志记录到 /var/log/wtmp 文件里。
halt 命令实现代码可以参考源文件
halt.c
ifdown.c
hddown.c
utmp.c
reboot.h
命令格式
/sbin/halt [-n] [-w] [-d] [-f] [-i] [-p] [-h]
主要选项
-n
reboot或者halt之前,不同步(sync)数据.-w
仅仅往/var/log/wtmp里写一个记录,并不实际做reboot或者halt操作.-f
强制halt或者reboot,不等其他程序退出或者服务停止就重新启动系统.这样会造成数据丢失,建议一般不要这样做.-i
halt或reboot前,关闭所有网络接口.-h
halt或poweroff前,使系统中所有的硬件处于等待状态.-p
在系统halt同时,做poweroff操作.即停止系统同时关闭电源.
poweroff 命令
poweroff 告诉内核中止系统并且关闭系统(参见 halt)
poweroff 命令实现代码可以参考源文件
halt.c (poweroff 是 halt 命令的软链接)
命令格式
poweroff [OPTION]...
主要选项
-f, --force
强制关机-p, --poweroff
等价于 halt -p-w, --wtmp-only
仅仅往/var/log/wtmp里写一个记录,并不实际做reboot或者halt操作.
reboot 命令
reboot 告诉内核重启系统(参见 halt)
reboot 命令实现代码可以参考源文件
halt.c (reboot 是 halt 命令的软链接)
命令格式
reboot [OPTION]...
主要选项
telinit 命令
telinit 告诉 init 该进入哪个运行级。执行 telinit 时,其实是调用 telinit 函数,通过向 INIT_FIFO (/dev/.initctl)写入命令 request 的方式通知 init 执行相应的操作。
telinit 命令实现代码可以参考源文件
init.c (telinit 是 init 命令的软链接)
命令格式
telinit [-t sec] [0123456sSQqabcUu]
参数说明
0,1,2,3,4,5,6 将运行级别切换到指定的运行级别。
a,b,c 只运行那些 /etc/inittab 文件中运行级别是 a,b 或 c 的记录。
Q,q 通知 init 重新检测 /etc/inittab 文件。
S,s 将运行级别切换到单用户模式下。
U,u 自动重启(保留状态),此操作不会对文件/etc/inittab 进行重新检测。执行此操作时,运行级别必须处在 Ss12345 之一,否则,该请求将被忽略。
-t sec 告诉 init 两次发送 SIGTERM 和 SIGKILL 信号的时间间隔。默认值是 5 秒
killall5 命令
killall5 命令发送一个信号到所有进程,但那些在它自己设定级别的进程将不会被这个运行的脚本所中断。
killall5 就是SystemV的killall命令。向除自己的会话(session)进程之外的其它进程发出信号,所以不能杀死当前使用的shell。
killall 命令实现代码可以参考源文件
killall5.c (telinit 是 init 命令的软链接)
命令格式
killall5 -signalnumber [-o omitpid[,omitpid..]] [-o omitpid[,omit‐pid..]..]
主要选项
-o omitpid
可以忽略的进程 pid 号
pidof
pidof 命令可以报告给定程序的进程识别号(pid),输出到标准输出设备。
这个命令其实是指向 killall5 的一个软链接。
pidof 命令实现代码可以参考源文件
killall5.c (pidof 是 killall 命令的软链接)
命令格式
pidof [-s] [-c] [-n] [-x] [-o omitpid[,omitpid..]] [-o omitpid[,omit‐pid..]..] program [program..]
主要选项
-s
表示只返回1个pid-o omitpid
表示告诉 piod 表示忽略后面给定的 pid ,可以使用多个 -o 。
last/lastb 命令
last 命令给出哪一个用户最后一次登录(或退出登录),它回溯/var/log/wtmp文件(或者-f选项指定的文件),显示自从这个文件建立以来,所有用户的登录情况。
lastb 显示所有失败登录企图,并记录在 /var/log/btmp.
last 命令实现代码可以参考源文件
last.c
oldutmp.h
命令格式
last [-R] [-num] [ -n num ] [-adFiowx] [ -f file ] [ -t YYYYMMDDHHMMSS] [name...] [tty...]
主要选项
-num(-n num)
指定 last 要显示多少行。-R
不显示主机名列。-a
在最后一列显示主机名(和下一个选项合用时很有用)-d
对于非本地的登录,Linux 不仅保存远程主机名而且保存IP地址。这个选项可以将IP地址转换为主机名。-i
这个选项类似于显示远程主机 IP 地址的 -d 选项,只不过它用数字和点符号显示IP地址。-o
读取一个旧格式的 wtmp 文件(用linux-libc5应用程序写入的)。-x
显示系统关机记录和运行级别改变的日志。
mesg 命令
该命令的作用是,控制是否允许在当前终端上显示出其它用户对当前用户终端发送的消息。
mesg 命令实现代码可以参考源文件
mesg.c
命令格式
mesg [y|n]
主要选项
y
允许消息传到当前终端n
不允许消息传到当前终端
mountpoint 命令
mountpoint 检查给定的目录是否是一个挂载点。
mountpoint 命令实现代码可以参考源文件
mountpoint.c
命令格式
/bin/mountpoint [-q] [-d] /path/to/directory
/bin/mountpoint -x /dev/device
主要选项
-q Be quiet - don't print anything.
fstab-decode 命令
fstab-decode 可以支持在运行命令时,将某些命令参数展开。
fstab-decode 命令实现代码可以参考源文件
fstab-decode.c
命令格式
fstab-decode COMMAND [ARGUMENT]...
举例
fstab-decode umount
runlevel 命令
runlevel 命令读取系统的登录记录文件(一般是/var/run/utmp)把以前和当前的系统运行级输出到标准输出设备。
如果之前的系统运行级别没有找到,则会返回一个 N 字母来代替。
runlevel 命令实现代码可以参考源文件
runlevel.c
命令格式
runlevel [utmp]
主要选项
utmp 指定要读取的 utmp 文件名,默认是读取 /var/run/utmp
sulogin 命令
sulogin 命令允许 root 登录,它通常情况下是在系统在单用户模式下运行时,由 init 所派生。
sulogin 命令实现代码可以参考源文件
sulogin.c
命令格式
sulogin [ -e ] [ -p ] [ -t SECONDS ] [ TTY ]
主要选项
无
wall 命令
wall 命令说明
wall命令用来向所有用户的终端发送一条信息。发送的信息可以作为参数在命令行给出,也可在执行wall命令后,从终端中输入。使用终端输入信息时,按Ctrl-D结束输入。wall的信息长度的限制是20行。只有超级用户有权限,给所有用户的终端发送消息。
wall 命令实现代码可以参考源文件
wall.c
dowall.c
命令格式
wall [-n] [ message ]
用法
usage: wall [message]举例
wall "hello msg"
bootlogd 命令
bootlogd 命令把启动信息记录到一个日志文件。
bootlogd 命令实现代码可以参考源文件
bootlogd.c
链接时需要 -lutil
命令格式
/sbin/bootlogd [-c] [-d] [-r] [-s] [-v] [ -l logfile ] [ -p pidfile ]
主要选项
-d Do not fork and run in the background.
utmpdump 命令
utmpdump 命令以一种用户友好的格式向标准输出设备显示 /var/run/utmp 文件的内容。
utmpdump 命令实现代码可以参考源文件
utmpdump.c
命令格式
utmpdump [-froh] filename
主要选项
-f output appended data as the file grows.
Sysvinit 项目详细分析
在分析源码之前,我们可以先了解一下所有源代码文件的行数,以便我们对分析的工作量和重点有所认识。
可以看出,全部源码的代码行数合计约9313行,其中代码量最多的一个源文件是 init.c 程序,也就是我们要分析的核心程序,这个程序的代码行已经接近3000行。除了这个文件之外,最多的代码行文件就是 killall5.c 只有约1000行。
init 命令实现代码分析
init.c 文件中的数据分析
00106
00107 CHILD family = NULL; / The linked list of all entries /
00108 CHILD newFamily = NULL; /* The list after inittab re-read /
00109
00110 CHILD ch_emerg = { / Emergency shell /
00111 WAITING, 0, 0, 0, 0,
00112 "~~",
00113 "S",
00114 3,
00115 "/sbin/sulogin",
00116 NULL,
00117 NULL
00118 };
00119
00120 char runlevel = 'S'; / The current run level /
00121 char thislevel = 'S'; / The current runlevel /
00122 char prevlevel = 'N'; / Previous runlevel /
00123 int dfl_level = 0; / Default runlevel /
00124 sig_atomic_t got_cont = 0; / Set if we received the SIGCONT signal /
00125 sig_atomic_t got_signals; / Set if we received a signal. /
00126 int emerg_shell = 0; / Start emergency shell? /
00127 int wrote_wtmp_reboot = 1; / Set when we wrote the reboot record /
00128 int wrote_utmp_reboot = 1; / Set when we wrote the reboot record /
00129 int wrote_wtmp_rlevel = 1; / Set when we wrote the runlevel record /
00130 int wrote_utmp_rlevel = 1; / Set when we wrote the runlevel record /
00131 int sltime = 5; / Sleep time between TERM and KILL /
00132 char argv0; /* First arguments; show up in ps listing /
00133 int maxproclen; / Maximal length of argv[0] with \0 /
00134 struct utmp utproto; / Only used for sizeof(utproto.ut_id) /
00135 char console_dev; /* Console device. /
00136 int pipe_fd = -1; / /dev/initctl /
00137 int did_boot = 0; / Did we already do BOOT* stuff? /
00138 int main(int, char *);
00139
00140 /* Used by re-exec part /
00141 int reload = 0; / Should we do initialization stuff? /
00142 char myname="/sbin/init"; /* What should we exec /
00143 int oops_error; / Used by some of the re-exec code. /
00144 const char Signature = "12567362"; * Signature for re-exec fd *
init.c 中的 main 函数流程分析
我们从 init.c 中 main 函数的执行逻辑开始分析。在 main 函数中主要负责完成以下工作:
下一小节,我们将重点来介绍 init_main 函数。
init_main 函数流程分析
该函数主要完成的功能是: 切换运行级别,检查出错情况,接受信号,启动相应服务例程。
console_init 函数流程分析
该函数主要完成的功能是: 设置 console_dev 变量为一个可以工作的 console
函数执行流程分析:
read_inittab 函数流程分析
该函数主要完成的功能是: 读取 /etc/inittab 文件,解析其中的约定规则,形成一个 CHILD 链表数据结构中。
该函数中用到的重要数据结构有 CHILD (struct child) 和 actions 数组 (struct actions)
CHILD 机构体 (struct child)
这个链表数据结构在 init.h 头文件中,是实现根据 init 运行级别加载不同用户程序的最重要的数据结构。
actions 数组 (struct actions)
这个数组保存的都是常量,包括常量字符串和宏定义,主要是一组对应关系,方便把 /etc/inittab 文件中的字符串转换为整型数。
例如 respawn -> RESPAWN, sysinit -> SYSINIT, initdefault -> INITDEFAULT
ACTIONS 宏定义
这一组宏定义的值,也是保存在 init.h 头文件中。
read_inittab 函数执行流程分析
start_if_needed 函数流程分析
该函数主要完成的功能是: 遍历 family 链表,调用 startup 启动链表上的子进程。
函数执行流程分析:
startup 函数流程分析
该函数主要完成的功能是: 执行 CHILD 节点所代表的配置行上的命令行,通常是个脚本程序。
函数执行流程分析:
spawn 函数流程分析
该函数主要完成的功能是: 调用 fork 和 execp 来启动子进程。这个函数非常长,但基本上是属于最底层的函数了。
函数执行流程分析:
boot_transitions 函数流程分析
该函数主要完成的功能是: 实现一个启动过程中所需要的状态机,完成状态的迁移。
函数执行流程分析:
check_init_fifo() 函数流程分析
该函数主要完成的功能是: 主要用于 init daemon 程序中,通过 select 函数监听来自于 /dev/initctl 管道的请求 request,分析并执行该请求 request。
函数执行流程分析:
struct init_request 请求协议格式
通过 /etc/initctl 管道 进行请求的数据,需要遵循一定的格式,也就是需要能够转换为如下的 init_request 结构体数据。
cmd 请求类别标识
所有正确的请求,都有一个唯一的标识,这些标识定义在 initreq.h 头文件中。
fail_check 函数流程分析
该函数主要完成的功能是: 在每次信号处理完成之后,遍历 family 链表检查每个节点的状态
函数执行流程分析:
SLEEEPTIME 数据
睡眠时间超过300秒=5分钟的进程,将会被清除标志位
process_signals 函数流程分析
该函数主要完成的功能是: 根据全局变量 got_signals 中哪些标志位被设置了,获得信号类型,进行相应的处理。
函数执行流程分析:
在 set.h 头文件中有关于这个宏定义的实现
通过 kill -l 可以得到这些 SIGXXXX 的具体赋值,如下:
console_stty 函数流程分析
该函数主要完成的功能是: 设置终端工作参数
函数执行流程分析:
fifo_new_level 函数流程分析
该函数主要完成的功能是: 真正完成改变 runlevel 的 request 请求,目标为传入参数 level,通过重新读取 inittab 文件来启动与新 runlevel 匹配的命令脚本。
函数执行流程分析:
re_exec 函数流程分析
该函数主要完成的功能是: 强制 init 程序重新执行。
函数执行流程分析:
get_init_default 函数流程分析
该函数主要完成的功能是: 查找 /etc/inittab 文件中的 initdefault 默认运行级别,如果有则返回,如果没有则请用户输入。
函数执行流程分析:
telinit 函数流程分析
在执行 telinit 函数时,实际上是通过向INIT_FIFO(/dev/initctl)写入命令的方式,通知 init 执行相应的操作。Telinit()根据不同请求,构造如下结构体类型的变量并向INIT_FIFO(/dev/initctl)写入该请求来完成其使命:
struct init_request {
int magic; /* Magic number /
int cmd; / What kind of request /
int runlevel; / Runlevel to change to /
int sleeptime; / Time between TERM and KILL */
union {
struct init_request_bsd bsd;
char data[368];
} i;
};
init进程执行流程分析
通过以上这些子函数的分析,我们可以总结一下关于 init 进程的运行状态和相应的执行流程。
init程序的3种启动执行方式
方式1 - Kernel 启动 init
在内核启动代码中,start_kernel 函数初始化代码的结束,会通过 command_line 来找出 init=execute_command 字符串中的程序来执行,或者按照默认的4个 init 程序的顺序依次来调用 execve() 执行 init 进程。这种方式启动的 init 进程,会完成读取 /etc/inittab 文件,建立 family 链表,依次执行各个子进程,并等待子进程的结束。当 init 进程运行到最后会进入一个无限循环中,变成一个 daemon init 进程。
这种启动方式,也是在整个操作系统启动过程中,init 程序的初次执行。这种启动是在内核空间启动 init 。
方式2 - 用户命令 telinit 启动 init
在 init 进程启动之后,用户通过终端可以完成登录进入 Bash 中,执行 telinit 命令的时候,因为 telinit 命令本身就是一个指向 init 程序的软链接,所以会导致 init 程序再次被执行。通过这种方式运行起来的 init 进程,因为 pid != 1 因此可以判断不是 Kernel 创建的 init 进程,此时会转为调用 telinit() 函数来执行。
这种情况下,telinit() 函数只负责打开 INIT_FIFO(/dev/initctl) 并按照传入参数,组织为一个 struct request 结构体,写入 FIFO 中,通知方式1中的 init 进程,就完成任务了。
这种启动方式通常会涉及到 runlevel 的切换,例如执行 telinit 1 或者 init 1 就会引起系统切换到单用户模式下。这是 init 程序的第2种常用的启动方式。我们可以看成是在用户空间启动 init 。
方式3 - 在程序中通过 re_exec() 函数启动 init
这种方式发生在通过方式1启动了init之后,在 init 执行的最后,进入了一个无限循环等待中。此时,用户如果在终端下执行 telinit U 命令,则代表着用户希望 re-execute itself,那么在方式2启动 init 之后,新的 init 进程会发送 U 命令给方式1启动的 init 进程,这个最原始的进程在循环中会调用 process_signals() 来处理 U 命令,处理方法是调用 re_exec() 函数。在这个函数中,会 fork 出一个子进程,子进程通过管道向父进程发送消息,由父进程通过 execle() 重新执行 init 程序,并传递 --init 参数,强制 init 重新执行。
和方式1的执行所不同的是,方式1在执行的后期,会读取 /etc/inittab 文件,建立 family 链表;而方式3因为是用户通过 telinit U 的方式告诉 方式1启动的那个 daemon init 进程,调用 re_exec() 函数,因此最原始的那个 init 进程,不会进行之前的 read_inittab() 初始化操作,而是直接进入到无限循环,又一次进入 daemon 的等待/处理循环中。
三种方式的相互联系
我们用一张总图来表示上述3种启动 init 的方式之间的相互联系。
图中红色部分的线路表示方式2和方式1之间的联系。
图中蓝色部分的线路表示方式3和方式1之间的联系。
方式2和3之间没有通信。
相同点
方式2和3都是通过 Bash 命令启动 init 进程。
方式2和3都是通过 INIT_FIFO 来和 Daemon init 进程进行通信。
不同点,方式2和3传递的参数不同。
方式2主要是传递新的优先级,例如 init 1, 表示要切换 init 进程到新的优先级1上工作。
方式3主要是要求重新启动init进程,并不需要修改优先级,例如 telinit U。
三种方式的比较区别
通过方式1启动的 init 进程 pid=1。
通过方式2和3启动的 init 进程,pid 一定不是 1 ,所以这个进程和前面的这个 init 进程完全不同。它们是分别属于内核空间和用户空间的2个不同的进程(前者进程1其实应该称为内核线程,因为它是通过 kernel_thread() 创建出来的,而后者是通过 shell 在用户空间 fork 出来的,是真正的用户程序,只不过这个用户程序只做了一个向管道发送数据的操作。
通过方式3,又让 pid=1 的原始 init 进程调用了 re_exec() -> fork() -> execle() 来(让父进程pid=1)重新加载了一次的 init 进程,本质上其实都是 1 号进程。但 fork() 出的子进程,其实也算是一个 init 进程,只不过它只完成了向父进程的 STATE_PIPE 写一下状态信息就退出 exit(0) 了,生命期很短。
方式2和3虽然启动的参数不一样,但都是通过调用 telinit 函数,对 INIT_FIFO 进行写 request 的操作。
两者的 request 参数略有不同
request.cmd 都是 INIT_CMD_RUNLVL
request.runlevel 则 一个是 '1', 一个是 'U'
在 fifo_new_level() 函数中,有对这2者的不同处理,前者调用 read_inittab(), 后者调用 re_exec()。
因此程序从 fifo_new_level() 函数这里开始有了不同的分支流程。
方式2会沿着红色线路,执行流程如下
调用 read_inittab() -> 更新 family 链表
在 Daemon 循环中,从 start_if_needed() -> startup()
最后通过 spawn() 完成启动 inittab 文件中的子进程。
方式3会沿着蓝色线路,执行流程如下
调用 re_exec() -> make_pipe(STATE_PIPE) 创建管道(fd=11)
然后通过 fork() 分出1个子进程,这个子进程只负责给父进程 STATE_PIPE 管道发送状态信息
父进程紧接着通过 execle() 来重新启动 init 进程,并传递一个启动参数 --init,使得再次启动 init 进程的时候,isinit=1。
这时父进程会进入 check_pipe() 函数中检查管道 STATE_PIPE 中是否有数据,是否符合要求。
如符合,则设置 reload=1,调用 init_main() 再次进入 Deamon 状态。
这一次进入 Daemon 之前,只需要执行 start_if_needed 而无需执行 read_inittab(),也就是不需要再重新读取 /etc/inittab 文件。这是 re_exec 方式3和 Kernel 启动 init 方式1 之间的最大区别。
halt命令执行流程分析
halt 命令的数据分析
根据 halt 命令的参数,在 main 函数的实现中,引入了如下这些变量。
halt命令实现代码分析
参数解析
从下面的 switch-case 语句分支中,可以很清楚的看出 halt 命令参数和上述变量之间的对应关系。
不同参数对应着不同处理
后继的代码很简单,就是根据不同的参数,进行不同的函数调用来处理。我们用一张图来表示:
从图中可以看出:
reboot 是 halt 命令的软链接
poweroff 是 halt 命令的软链接,等价于 halt -p
halt 命令是依靠 execv("/sbin/shutdown") 调用 shutdown 命令来完成工作的。
shutdown 命令是关机的核心命令,其他命令都不如这个命令重要。
shutdown命令执行流程分析
shutdown命令的数据分析
shutdown命令实现代码分析
shutdown 的核心代码在 shutdown() 函数中,除了用 openlog(), syslog(), closelog() 来写关机日志外,主要是靠调用 execv(INIT, args) 来启动 init 进程完成改变运行级别的工作,从而间接完成关机操作。
sulogin命令执行流程分析
sushell函数代码分析
该函数主要完成的功能是: 当用户的密码验证通过后,启动一个shell (由环境变量SUSHELL指定)
函数执行流程分析:
getrootpwent函数代码分析
该函数主要完成的功能是: 获得根用户 root 的密码
函数执行流程分析:
getpasswd 函数代码分析
该函数主要完成的功能是: 从标准输入获得用户输入的密码
函数执行流程分析:
runlevel命令执行流程分析
utmpname 函数代码分析
该函数主要完成的功能是: 设置utmp 文件路径
utmpname()用来设置utmp文件的路径,以提供utmp相关函数的存取路径。如果没有使用utmpname()则默认utmp文件路径为/var/run/utmp。
setutent 函数代码分析
该函数主要完成的功能是: 从头读取utmp 文件中的登录数据
setutent()用来将getutent()的读写地址指回utmp文件开头。
getutent 函数代码分析
该函数主要完成的功能是: 从utmp 文件中取得账号登录数据
getutent()用来从utmp 文件(/var/run/utmp)中读取一项登录数据,该数据以utmp 结构返回。第一次调用时会取得第一位用户数据,之后每调用一次就会返回下一项数据,直到已无任何数据时返回NULL。
utmp结构定义
ut_type有以下几种类型
endutent 函数
该函数主要完成的功能是: 关闭utmp 文件
endutent()用来关闭由getutent所打开的utmp文件。
killall5 命令执行流程分析
killall5 是 sysvinit 工具软件包中,从实现代码量上是仅次于 init 命令的一个重要命令。
下面我们从 killall5.c 文件的 main 函数开始分析整个命令的执行流程。
killall5 主函数代码分析
获得执行程序名 progname
00986 /* Main for either killall or pidof. /
00987 int main(int argc, char *argv)
00988 {
00989 PROC p;
00990 int pid, sid = -1;
00991 int sig = SIGKILL;
00992 int c;
00993
00994 / return non-zero if no process was killed /
00995 int retval = 2;
00996
00997 / Get program name. */
00998 if ((progname = strrchr(argv[0], '/')) == NULL)
00999 progname = argv[0];
01000 else
01001 progname++;
01002
通过最开始一段代码,可以看出 sig = SIGKILL 默认的发送信号是 SIGKILL,这个信号是9号,默认处理动作是 exit。
progname 字符串指针是一个全局变量,经过赋值之后,获得了正在运行程序的名字,只是文件名,不包含路径名。
打开系统日志
下面这段代码是调用 openlog() 函数来打开以 progname 为日志文件名的日志文件。
处理 pidof 命令的情况
因为 pidof 是指向 killall5 的软链接,因此程序需要判断是通过执行 killall5 命令启动的,还是 pidof 命令启动的。
如果是 pidof 命令,则调用 main_pidof 的主函数来实现这个命令,并在执行完 main_pidof() 函数后直接返回,不再执行后继的代码。同时这个函数的返回值,也作为整个程序的返回值。
因为 pidof 命令在下面我们专门有一个小节来讨论,因此我们不在此展开,而是继续接着 killall5 命令的执行流程分析下去。
分析 -o omitpid 参数创建双向链表 omit
程序进行到此,确定是用户要执行 killall5 命令。同时给 omit 链表赋值为 0 ,表示此时链表为空。
下面是开始进行传入参数的匹配处理。因为 killall5 命令支持 -o 参数传递 omitpid ,也就是可以被忽略的进程不进行 kill 操作,因此要取出 argv[] 参数列表中的所有 omitpid 。
从代码中可以看出,strsep(&hear, ",;:") 其中以逗号,分号,冒号作为分隔符,以 omit 为链表头,不断分析 argv[] 数组中和 -o 有关的 pid 列表,然后负责用动态生成节点的方式,创建链表。
这段代码中,通过 xmemalign() 函数,动态进行内存分配,并且按对齐方式分配内存,以在链表头部插入新的节点的方法建立 omit 链表。其中要用到一个链表节点的数据结构 OMIT ,这是在 killall5.c 头部定义的一个结构体。这个结构体只有一个成员变量就是 pid_t pid,其他2个成员是为了建立一个双向链表而必须引入的 next 和 prev 指针。
挂载 /proc 文件系统
接下来,程序通过调用 mount_proc() 函数来挂载 /proc 文件系统,确保接下来能够访问 /proc 文件系统的节点。
这个函数的分析,这里不再展开。简单来说,就是如果 /proc 文件系统已经存在,则可以读取 /proc/version 。如果不存在,则调用 excev("/sbin/mount", args) 来完成程序加载。这里 args 传递的mount参数是 "-t", "proc", "proc", "/proc", 0 。
暂时忽略 SIGTERM/SIGSTOP/SIGKILL 信号
考虑到后面会通过调用 kill(-1, SIGSTOP) 的方式来停止所有进程,因此这里为了确保即使以后 kill(-1, ...) 的调用不会把执行进程也暂停掉,先给这3个信号注册了一个 SIG_IGN 处理函数,表示忽略掉这些信号。
禁止内存换出,暂停所有进程
因为内存管理机制会对暂时处于停止状态的进程进行换出内存的操作,因此提前先禁止这一功能。然后再执行 kill(-1, SIGSTOP) 给所有进程发送 SIGSTOP 信号。
读 /proc 文件系统,建立进程链表 plist
通过 readproc() 函数来读取 /proc 文件系统的关于运行进程的节点,并将所有进程填入组成一个进程链表 plist 。
在这里,用到了一个全局变量,进程链表头 plist 指针,接下来所有链表的节点都会插入到表头 plist 所在的位置。
readproc() 函数调用会通过读取 /proc 文件系统的目录和文件的方法,将所有进程的pid 信息填入并组建 plist 链表。
这个 PROC 结构体中,包含了有关正在运行的进程信息,例如 pid, ino, sid, dev 等。
根据 plist 链表开始依次 kill 进程
这段代码是实现 killall5 命令最核心的部分,逻辑也很简单,就是遍历 plist 链表,对于每一个正在运行的进程,首先判断它是否是 1号 init 进程,当前进程或者当前Session进程,或者是内核线程,如果是这些则忽略过不进行 kill 。
接着判断该进程是否属于 omit 链表中要求被忽略的进程之一,如果是这些用户指定的要忽略的进程,则也不进行 kill 。
如果不属于以上这2种情况,则调用 kill(p->pid, sig) 来完成 kill 操作。
恢复所有进程运行 (从 STOP 又回到 CONT)
发送完 SIGKILL 之后,调用 kill 函数给每个刚才要求 SIGSTOP 的进程发送 sig=SIGCONT 信号。
这个引号要求所有刚才被暂停的进程,恢复执行后尽快接收到 SIGCONT 继续运行 (continue),
当再次收到 SIGKILL 的时候,可以认为是系统要求结束所有进程,然后由各个进程自己结束。
关闭日志
通过调用 closelog() 函数来正常关闭日志,日志的文件是 /var/log/sysvinit。
调用 usleep(1) 强制 Kernel 运行调度器
强制内核必须要使用 usleep(1) 以便能够让出 CPU,从而要求使用调度器进行调度。
pidof 命令执行流程分析
pidof 命令的实现也是在 killall5.c 文件中,由 main_pidof() 主函数负责完成。下面我们详细分析这个函数的实现。
main_pidof 主程序代码分析
初始化和数据结构定义
00852 int main_pidof(int argc, char *argv)
00853 {
00854 PIDQ_HEAD q;
00855 PROC p;
00856 char token, here;
00857 int f;
00858 int first = 1;
00859 int opt, flags = 0;
00860 int chroot_check = 0;
00861 struct stat st;
00862 char tmp[512];
00863
00864 omit = (OMIT)0;
00865 nlist = (NFS*)0;
00866 opterr = 0;
00867
这里同样引入了 2 个重要的链表结构,PROC *p 和 PIDQ_HEAD *q 。其中 PROC 结构体前面已经介绍过,PIDQ_HEAD 的结构体定义如下:
这是一个双向链表,head 和 tail 指针分别指向头节点和尾节点。而 PIDQ 本身就是一个单向链表,里面的元素是 PROC 结构体。
参数处理并完成 omit 进程号链表
接下来部分的代码主要负责处理传入参数,特别是通过 -o omitpid,... 方式传进来的可以被忽略的进程号,并将它们组成一个 omit 链表。其他部分的处理,基本都是给 flags 变量进行相关置位 setbit 操作。
检查对于 /proc/pid/root 的访问权限
00920 /* Check if we are in a chroot */
00921 if (chroot_check) {
00922 snprintf(tmp, 512, "/proc/%d/root", getpid());
00923 if (stat(tmp, &st) < 0) {
00924 nsyslog(LOG_ERR, "stat failed for %s!\n", tmp);
00925 closelog();
00926 exit(1);
00927 }
00928 }
检查 fs 是否基于 nfs 的网络文件系统
00930 if (flags & PIDOF_NETFS)
00931 init_nfs(); /* Which network based FS are online? */
00932
读 /proc 文件系统,建立进程链表 plist
00933 /* Print out process-ID's one by one. */
00934 readproc((flags & PIDOF_NETFS) ? DO_NETFS : DO_STAT);
00935
调用 pidof 函数,返回进程名为 argv[f] 进程 PIDQ_HEAD 链表
00936 for(f = 0; f < argc; f++) {
00937 if ((q = pidof(argv[f])) != NULL) {
00938 pid_t spid = 0;
00939 while ((p = get_next_from_pid_q(q))) {
00940 if ((flags & PIDOF_OMIT) && omit) {
00941 OMIT * optr;
00942 for (optr = omit; optr; optr = optr->next) {
00943 if (optr->pid == p->pid)
00944 break;
00945 }
00946
00947 /*
00948 * On a match, continue with
00949 * the for loop above.
00950 */
00951 if (optr)
00952 continue;
00953 }
00954 if (flags & PIDOF_SINGLE) {
00955 if (spid)
00956 continue;
00957 else
00958 spid = 1;
00959 }
00960 if (chroot_check) {
00961 struct stat st2;
00962 snprintf(tmp, 512, "/proc/%d/root",
00963 p->pid);
00964 if (stat(tmp, &st2) < 0 ||
00965 st.st_dev != st2.st_dev ||
00966 st.st_ino != st2.st_ino) {
00967 continue;
00968 }
00969 }
00970 if (!first)
00971 printf(" ");
00972 printf("%d", p->pid);
00973 first = 0;
00974 }
00975 }
00976 }
972 行的这行代码至关重要,正是由这句话负责打印输出了 pidof 命令的输出结果,也就是每个符合匹配进程名的进程号。
退出前的清理工作
最后,程序在退出前,处理是否打印回车,卸载mount上来的 /proc 文件系统,关闭系统日志等。如果成功匹配并输出了进程名,则返回0,否则返回值为1表示没有用户指定名字的进程,不能打印输出其进程号。
readproc 函数代码分析
改变目录到 /proc ,调用 opendir 打开当前目录文件 .
00450 int readproc(int do_stat)
00451 {
00452 DIR dir;
00453 FILE fp;
00454 PROC p, *n;
00455 struct dirent d;
00456 struct stat st;
00457 char path[PATH_MAX+1];
00458 char buf[PATH_MAX+1];
00459 char s, *q;
00460 unsigned long startcode, endcode;
00461 int pid, f;
00462
00463 / Open the /proc directory. */
00464 if (chdir("/proc") == -1) {
00465 nsyslog(LOG_ERR, "chdir /proc failed");
00466 return -1;
00467 }
00468 if ((dir = opendir(".")) == NULL) {
00469 nsyslog(LOG_ERR, "cannot opendir(/proc)");
00470 return -1;
00471 }
释放已经存在的 plist 链表,为重新生成做好准备
00473 /* Free the already existing process list. */
00474 n = plist;
00475 for (p = plist; n; p = n) {
00476 n = p->next;
00477 if (p->argv0) free(p->argv0);
00478 if (p->argv1) free(p->argv1);
00479 if (p->statname) free(p->statname);
00480 free(p);
00481 }
00482 plist = NULL;
开始遍历 /proc 目录,找出是进程号(大于0的数字)的文件名
00484 /* Walk through the directory. /
00485 while ((d = readdir(dir)) != NULL) {
00486
00487 / See if this is a process /
00488 if ((pid = atoi(d->d_name)) == 0) continue;
00489
00490 / Get a PROC struct . /
00491 p = (PROC )xmalloc(sizeof(PROC));
00492 memset(p, 0, sizeof(PROC));
00493
00494 /* Open the status file. */
00495 snprintf(path, sizeof(path), "%s/stat", d->d_name);
00496
以 bash 文件为例,查看进程的 stat 文件信息,找出进程名
$ cat /proc/17500/stat
17500 (bash) S 17492 17500 17500 34816 17500 4202496 20014 5614681 596 17984 91 163 41923 13952 20 0 1 0 4974766 8609792 413 4294967295 134512640 135410880 3214451152 3214448040 3078423588 0 0 3686404 1266761467 3241494555 0 0 17 0 0 0 174 0 0
$
以下代码完成从 /proc/pid/stat 文件中读取内容,找出 ( ) 左右园括号之间的进程名称 statname 存入 PROC 结构体 p 中。
得到进程 session id,判断其是否 kernel thread
00530 /* Get session, startcode, endcode. /
00531 startcode = endcode = 0;
00532 if (sscanf(q, "%c %d %d %d %d %d %u %u "
00533 "%u %u %u %u %u %d %d "
00534 "%d %d %d %d %u %u %d "
00535 "%u %lu %lu",
00536 &p->sid, &startcode, &endcode) != 3) {
00537 p->sid = 0;
00538 nsyslog(LOG_ERR, "can't read sid from %s\n",
00539 path);
00540 if (p->argv0) free(p->argv0);
00541 if (p->argv1) free(p->argv1);
00542 if (p->statname) free(p->statname);
00543 free(p);
00544 continue;
00545 }
00546 if (startcode 0 && endcode 0)
00547 p->kernel = 1;
00548 fclose(fp);
00549 } else {
00550 / Process disappeared.. */
00551 if (p->argv0) free(p->argv0);
00552 if (p->argv1) free(p->argv1);
00553 if (p->statname) free(p->statname);
00554 free(p);
00555 continue;
00556 }
获得其他参数
因为后继代码比较长,就不再一一列出,下面以 smbd 进程为例,总结一下 /proc 文件系统中的相关信息。
stat 文件包含进程号,进程名
在对 stat 文件分析过之后,对 PROC 结构体 p 指针的成员进行赋值。
cmdline 文件包含运行时参数
在对 stat 文件分析过之后,对 PROC 结构体 p 指针的成员进行赋值。
exe 文件路径中包含设备节点号和 inode节点号
在对 stat 文件分析过之后,对 PROC 结构体 p 指针的成员进行赋值。
添加进入链表,并完成收尾工作
00623 /* Link it into the list. /
00624 p->next = plist;
00625 plist = p;
00626 p->pid = pid;
00627 }
00628 closedir(dir);
00629
00630 / Done. */
00631 return 0;
00632 }
pidof 函数代码分析
该函数主要完成的功能是: 从用户输入的程序名 prog 字符串,得到一个和这个名称相同的进程号链表 PIDQ_HEAD 指针返回
函数执行流程分析:
匹配算法见 killall5.c 文件的 757行
Sysvinit 项目安全漏洞
案例1-umask掩码安全漏洞
Linux内核的某些版本中,在umask设置为0000的情况下生成init进程。在这些版本的Linux,有些进程的初始化数据依赖于“init”的umask,并不自行设置。因为init进程生成系统重要而且敏感的文件,因而这种情况就可能形成漏洞问题。如果成功利用该漏洞,恶意用户就可以获取root权限,并破坏系统。
以下是可能会受到影响的 Linux 内核版本:
umask用法简介
umask和创建文件时的默认读写执行的权限相关,是和chmod命令密切相关的。
我们知道用 ls -l 命令查看文件权限,总共为4位(gid/uid,属主,组权,其它用户的权限),不过通常用到的是后3个。
例如,可以用chmod 755 file(此时这文件的权限是属主读(4)+写(2)+执行(1),同组的和其它用户有读写权限)
umask的作用权限掩码
默认情况下的umask值是022,可以用umask命令查看默认值。
此时,建立的文件默认权限是644,建立的目录的默认权限是755,因为建立文件的时候,默认本来就是没有可执行权限x的,而建立目录的时候,默认是可以执行的(也就是可以用 cd 进入)。
知道了umask的作用后,就可以修改umask的值了,例如:umask 024 则以后建立的文件和目录的默认权限就为642,753了
参考资料
案例2-利用脚本和符号链接攻击系统目录
这个安全漏洞描述,是在 Debian 社区邮件列表中提出的。参考信息 BUGTRAQ ID: 52198
因为这个漏洞是在于 init 执行脚本 x11-common 时,创建了一个临时目录,然后将临时目录的权限改为1777。这里,因为 init 脚本的执行是内核启动,以 root 权限执行的,所以执行脚本中的命令行时,也是 root 权限,就可能会造成安全漏洞。
举例
例如,如果用户提前根据创建一个符号链接文件,名字和
这个漏洞产生的原因是,由于init脚本"x11-common"不安全方式创建"/tmp/.X11-unix"和"/tmp/.ICE-unix",攻击者可以通过符号链接进行攻击,可能获得root特权。
参考资料
案例3-Android root 提权漏洞
这篇文章中所提到的安全漏洞,也是和 init 进程有一定关系,但是并不在我们分析的 sysvinit 软件包中,也无法进行验证,仅供参考。
这个安全漏洞中,提到了一个 uevent 数据结构,是在 Linux 2.6 内核中设备驱动程序模型的一个重大调整。uevent事件(以前叫hotplug事件) 被用户出发后,会向内核传递消息,内核收到消息后,会进行解析和处理。
举例
文中提到了这样一条消息
通过函数parse_event进行解析后,uevent的结构为:
最后在内核load_firmware函数,把hotplug中的数据写到 /proc/sys/kernel/hotplug 中,其内容变为 /data/local/tmp/exploid。这样就完成了让内核自动加载恶意程序,只要再次受到如wifi打开、usb插入等热拔插信息,内核就会以root权限加载恶意程序执行,从而达到提权的目的。
参考资料
结合init源码剖析android root提权漏洞
Sysvinit 项目运行时调试图
Linux 内核启动 init 进程运行调试图
内核代码并不在 sysvinit 软件包项目分析工作之内,但是对于理解 init 进程是如何启动的,至关重要。因此我们分析了 Linux 内核的源代码,找到其中从内核启动函数 start_kernel 开始,到 /sbin/bin 进程被调用的执行路径和运行逻辑流程。
最后我们通过一张运行时调试图,来表示其中用到的相关函数调用关系,以及和 init 进程之间的交互。
start_kernel
这个函数是内核从汇编程序跳转到C程序的入口。内核里面并没有 main 函数,start_kernel 就相当于内核的 main 函数,从这里开始,内核开始进入了 C 语言级别的初始化操作。同时,创建了内核线程调用了 init() 函数,最终完成了启动 init Daemon 进程的工作。
这个内核函数是在 init/main.c 中,其中 558 行的 parse_options 负责处理通过内核启动时的命令行参数传递的 init=xxxx 的信息。
例如在嵌入式Linux系统中,通常 init=/linuxrc 脚本程序。
其中 628行处,rest_init() 函数,负责启动 kernel_thread 内核线程,后面我们会分析这个函数。
parse_options
在这个函数中 451 行处,开始进行 init=xxxx 字符串的解析,并将 xxxx 传递给 execute_command 指针,后面就会用到这个指针。
rest_init
进入 rest_init() 函数后的最重要的工作,就是负责启动内核线程,由它来完成调用 init() 内部函数的工作。
init 函数
在这个 init() 函数中,通过 execve 系统函数,实现了加载 /sbin/init 进程来运行的任务,最终完成启动 init 进程。
从 833 行可以看出,内核启动 init 程序,搜索该程序文件的顺序是,先看用户传入的命令行参数 init=xxxx 里面的指定程序,然后再是 /sbin/init, /etc/init, /bin/init, /bin/sh 这4个默认的程序,如果都找不到,则会内核报错 panic,No init found. 内核启动失败。
至此我们找到了一条路径,使得内核从 start_kernel 的主函数,进入到 init 进程。这里涉及到了4个重要的函数和1个重要的变量,这些都是和 init 进程如何启动直接相关的,对于我们了解在 init 进程启动之前的逻辑流程有重要作用。
start_kernel()
parse_options()
rest_init()
init()
execute_command
我们用下面这张图来表示这些函数和变量之间的关系,可以更直观的看到内核启动init进程的流程。
init 进程和 telinit 之间的运行调试图
修改 INIT_FIFO 改变两者联系的管道
修改 initreq.h 头文件 33行处,定义为 /tmp/.initctl 以防和现在系统运行的 init 进程有关联
修改 Makefile ,加上 -DDEBUG 选项
修改 Makefile 13行处,增加一个 -DDEBUG
11 CPPFLAGS =
12 CFLAGS ?= -ansi -O2 -fomit-frame-pointer
13 override CFLAGS += -W -Wall -D_GNU_SOURCE -DDEBUG
14 STATIC =
查看 init.h 头文件 64行处,INITDBG 通过 initlog 函数输出,无需修改
修改 reboot 操作为打印语句,以防系统重启
修改 reboot.h 头文件 50行处,将 init_reboot 宏,修改为打印语句
修改源码中信号处理函数,以便直接中断 init 执行
修改 init.c 源文件 102行处,注释掉注册信号处理函数的代码部分
修改源码中关闭标准输出的部分,可以显示出调试信息
1049 #if 0
1050 close(0);
1051 close(1);
1052 close(2);
1053 #endif
在 Daemon 程序中插入打印当前获得 request 的信息
重新编译运行 sudo ./init -i
此时 init 进入 Daemon 循环中
启动 telinit 1 要求切换运行级别为单用户
我们运行了3次,分别要求改变运行级别为 1,5,7,这样就会通过 INIT_FIFO 发送3次数据。
查看 init 接收请求和处理方法
可以看出通过 INIT_FIFO ,init 方式2启动后,发送request请求,Deamon init 收到后可以打印出这个请求相关信息。
和请求级别相对应的,Daemon 也分别修改了3次级别,并仍然处于接收下一个 request 请求的循环中。
INIT_FIFO管道文件调试图
因为 init Daemon 进程,是通过 INIT_FIFO (/dev/.initctl) 管道来进行通讯,而这个管道的名字是编译时就指定好的,在整个系统运行过程中,这个管道名是固定不变的。因此只需要用户程序有权限来对这个管道进行写操作,就可以获得对 init 进程的控制。
我们来做一个实验,来模拟测试一下这个工作机制。
修改默认管道文件
修改源码文件 initreq.h +35行,将 INIT_FIFO 定义为 /tmp/.initctl
修改管道文件权限
将管道文件的权限改为 777 ,允许任意用户进行读写
管道传送结构体定义
参考 initreq.h 头文件中的定义,按照结构体 struct init_request 的格式,向管道中写入数据
其中第1个int型(4字节)是 Magic Number,这是一个固定值 0x03091969 。考虑到小尾端的存储方式,数据的前4个字节应该是16进制的 69 19 09 03 倒过来存放的。
第2个int型(4字节)是 cmd ,如果我们选择要更改 runlevel ,则对应的值是 INIT_CMD_RUNLVL = 1,内存里面的顺序为 01 00 00 00
第3个int型(4字节)是 runlevel , 如果我们向修改的运行级别是 3 级,则对应的值是字符(char) '3',内存里面的顺序为 33 00 00 00
制作数据文件
工具准备:安装 Ubuntu 上的16进制编辑器
使用 dd 命令制作一个用于 INIT_FIFO 传送的 request 数据包
struct init_request 的大小为384个字节,每次传送 sizeof(struct init_request) 给 Daemon 表示要进行的操作。
使用 tweak 命令,来用 16进制方式修改 request.dat 文件
测试通过 INIT_FIFO 来传送 request 数据包
参考运行时调试图的第2个实验,用本地执行 sudo ./init -i 0 的方式来启动一个模拟的 Daemon 进程。
此时,开启另外一个终端窗口,通过 cat request.dat > /dev/.initctl 传送 request 请求包
观察 Daemon init 进程输出的调试信息,可以看到 init 进程已经识别出有一个修改 runlevel 的请求。
最后编辑:SteveChen 更新时间:2025-04-06 13:56