HaiPeng(lzuzhp@gmail.com)
一台PC机,CPU是核心,对于操作系统,管理CPU的那部分便是OS的核心,这就是进程管理,我就认为“得进程管理者得linux内核”,OS的其他资源(内存、磁盘、网络等)都要提供该该资源的操作函数来供进程来使用。
打印内核中的所有进程
通过ulk我们知道,linux内核的所有进程是通过双向链表串在一起的,而且每一个进程都有一个进程描述符来代表(其实就是一个结构体struct,只不过内核中的结构体贼多,而且进程的结构体又常用,所以就给它另外起了个名字),这个描述符就是struct task_struct,该结构体包含了一百多个元素,就像人(一个进程代表一个人,内核中有很多进程,就像一个工厂)一样是由很多部分组成的,解剖的时候当然想知道每一个人每一个部分的状态,为了达到我们的目地,我们继续读ulk(目前我认为只要自己想到的,前人一定做过了),发现3.2.2.4专门是将进程中的双向链表的,for_each_process便可以获得内核中的每一个进程,但是怎么用这个宏呢?查看内核源代码(使用sourceinsight即可),看看内核是怎么使用的,内核函数check_for_tasks使用了该宏,原来很简单,只需要定义一个struct task_struct *p的结构体指针便可以啦。但是如何写代码呢,我的方法是使用systemtap的embedded c,这是目前我找到的效率最高的方法啦。使用systemtap写的脚本语言如下:
1//process_list.stp
2 %{
3 #include <linux/list.h>
4 #include <linux/sched.h>
5 %}
6 function process_list ()
7 %{
8 struct task_struct *p;
9 struct list_head *_p,*_n;
10 for_each_process(p){
11
12 _stp_printf("%-15s (%-5d)\n",p->comm,p->pid);
13 }
14 %}
15 probe begin
16 {
17 process_list();
18 exit()
19 }
运行的时候使用命令:sudostap –gv process_list.stp便可以看到你的内核中的所有进程啦。输出结果(部分):
Pass 5: starting run.
init (1 )
kthreadd (2)
ksoftirqd/0 (3 )
migration/0 (4 )
watchdog/0 (5 )
events/0 (6 )
cpuset (7 )
khelper (8 )
netns (9 )
async/mgr (10 )
pm (11 )
sync_supers (12 )
bdi-default (13 )
kintegrityd/0 (14 )
kblockd/0 (15 )
kacpid (16 )
如果你不明白那个systemtap脚本语言的含义,没关系,只要systemtap已经在你的机子上安装好了就行了,后面用着的多啦也就熟练啦。想学systemtap的话可以参见我的《systemtap使用日记》。
目前我们仅仅是打印了structtask_struct结构体中的两个元素,至于其他的留给你们自己去慢慢“解剖”吧。
Namespace初探
PLKA一书的2.3.2是介绍namespace的,namespace是一项虚拟化技术,它是内嵌在进程描述符中的,所以linux内核中的namespace是为进程服务的。凡是在内核中被namespace过得资源,内核中的所有进程都是可见的,下面我们就来做个简单的实验验证一下,进而加深对namespace的印象。
在完成了“打印内核中的所有进程”这一任务之后,完成这个任务便容易一些啦。PLKA的2.3.2接介绍了UTS namespace,我们就打印这个UTS namespace,看看是否所有的进程打印出来的都一样。使用systemtap编写的内核脚本如下:
1 //namespace_uts.stp
2 %{
3 #include<linux/list.h>
4 #include<linux/sched.h>
5 #include <linux/nsproxy.h>
6 #include<linux/utsname.h>
7 %}
8function process_list ()
9 %{
10 struct task_struct *p;
11 struct list_head *_p,*_n;
12 struct uts_namespace *uts;
13 struct new_utsname *utsname;
14
15 for_each_process(p){
16 uts=p->nsproxy->uts_ns;
17 utsname=&(uts->name);
18 _stp_printf("%-15s(%-5d) %s\n",
p->comm,p->pid,utsname->release);
19 }
20 %}
21probe begin
22 {
23 process_list();
24 exit()
25 }
运行的命令是:sudo stap –gvnamespace_uts.stp,在程序中我们仅仅是打印了内核版本(struct new_utsname结构体中的release[65]子项),打印出来的结果如下图(部分):
Pass 5: starting run.
init (1 ) 2.6.35-22-generic
kthreadd (2 ) 2.6.35-22-generic
ksoftirqd/0 (3 ) 2.6.35-22-generic
migration/0 (4 ) 2.6.35-22-generic
watchdog/0 (5 ) 2.6.35-22-generic
events/0 (6 ) 2.6.35-22-generic
cpuset (7 ) 2.6.35-22-generic
khelper (8 ) 2.6.35-22-generic
netns (9 ) 2.6.35-22-generic
async/mgr (10 ) 2.6.35-22-generic
pm (11 ) 2.6.35-22-generic
sync_supers (12 ) 2.6.35-22-generic
bdi-default (13 ) 2.6.35-22-generic
kintegrityd/0 (14 ) 2.6.35-22-generic
kblockd/0 (15 ) 2.6.35-22-generic
kacpid (16 ) 2.6.35-22-generic
kacpi_notify (17 ) 2.6.35-22-generic
kacpi_hotplug (18 ) 2.6.35-22-generic
ata_aux (19 ) 2.6.35-22-generic
ata_sff/0 (20 ) 2.6.35-22-generic
从结果来看,所有的进程打印出来的均是2.6.35-22-generic,这就是由内核中的namespace机制造成的,这也许就是虚拟化的本质吧。
获取内核调度轨迹以及切换频率
在读进程切换的时候我就在想能不能获得发生切换的进程信息呢?这样就能够知道在cpu上运行的进程的信息了。方法依然是采用systemtap脚本语言,加上该脚本语言的特殊性,我们很容易获得每个cpu上的进程切换频率(针对多核CPU)。脚本如下:
1//sched_switch.stp
2 global switch_frequency[10000],begin_time,end_time
3 probe begin
4 {
5 begin_time=gettimeofday_us();
6 }
7 probe kernel.function("__switch_to")
8 {
9 printf("cpu:%d time:%dus%s(%d)=>%s(%d)\n",cpu(), \
gettimeofday_us(),kernel_string($prev_p->comm),$prev_p->pid,\
kernel_string($next_p->comm),$next_p->pid);
10 switch_frequency[cpu()] <<< 1
11 }
12 probe timer.ms(1000)
13 {
14 ansi_clear_screen();
15 end_time=gettimeofday_us();
16 delt=(end_time-begin_time)/1000000;
17 foreach([cpu] in switch_frequency){
18 printf("cpu %d switch frequency is :%d/s\n",cpu, \
@sum(switch_frequency[cpu])/delt);
19 }
20 exit();
21 }
在统计每个CPU核上的进程切换频率时,主要采用了systemtap的数组方法(就是switch_frequency[cpu()]<<< 1),运行命令:sudo stap –gv sched_switch.stp。输出结果(部分):
Pass 5: starting run.
cpu:1 time:1310900356795828usstapio(1874)=>swapper(0)
cpu:1 time:1310900356796081usswapper(0)=>rsyslogd(650)
cpu:0 time:1310900356796820usswapper(0)=>rsyslogd(642)
cpu:1 time:1310900356797067usrsyslogd(650)=>swapper(0)
cpu:0 time:1310900356797540usrsyslogd(642)=>swapper(0)
cpu:0 time:1310900356802798usswapper(0)=>VBoxService(1007)
cpu:0 time:1310900356803464usVBoxService(1007)=>swapper(0)
cpu:1 time:1310900356816680usswapper(0)=>events/1(10)
cpu:1 time:1310900356816813usevents/1(10)=>swapper(0)
…
…
…
cpu 0 switch frequency is :86/s
cpu 1 switch frequency is :116/s
Pass 5: run completed in20usr/130sys/1423real ms.
现在我们就可以知道每时每刻每个CPU核上运行的进程,基于此,当你再回首去读内核源代码的时候,新的想法可能就又诞生啦。
内核函数参数传递
函数的参数传递有值传递和地址传递,传参的方式有寄存器传参和栈传参,那么linux是采用哪种方式呢?下面的实验将为你揭晓:
(gdb) b dump_trace
Breakpoint 2 at 0xc10058fe: filearch/x86/kernel/dumpstack_32.c, line 30.
(gdb) c
Continuing.
Breakpoint 2, dump_trace (task=0xc164acc0, regs=0x0,stack=0x0, bp=0, ops=0xc147dc24, data=0xc1645df4) atarch/x86/kernel/dumpstack_32.c:30
30 {
(gdb) info register
eax 0xc164acc0 -1050366784
ecx 0x0 0
edx 0x0 0
ebx 0xc1645df4 -1050386956
esp 0xc1645da8 0xc1645da8
ebp 0xc1645dcc 0xc1645dcc
esi 0xc17f5000 -1048621056
edi 0xc16e4f6c -1049735316
eip 0xc10058fe 0xc10058fe<dump_trace+14>
eflags 0x92 [ AF SF ]
cs 0x60 96
ss 0x68 104
ds 0x7b 123
es 0x7b 123
fs 0xd8 216
gs 0xe0 224
(gdb) x/16xw 0xc1645da8
0xc1645da8: 0x00000000 0xc147dc24 0xc1645dc8 0x00000093
0xc1645db8: 0xc1645de4 0xc16be172 0xc1645df4 0xc17f5000
0xc1645dc8: 0xc16e4f6c 0xc1645de4 0xc100d380 0x00000000
0xc1645dd8: 0xc147dc24 0xc1645df4 0x00000000 0xc1645e10
我们使用GDB+QEMU来达到我们的目的(有关GDB+QEMU调试内核的参考手册强参见前文所说),我们在函数dump_trace的入口处设置断点,通过info register可以查看断点处的寄存器值,我们发现dump_strace的前三个参数是分别通过eax,ecx,edx来传递的,而其他三个参数呢?我们通过命令x/16xw esp_value来获得此刻栈上的信息,发现其他三个参数在栈上!至此,我们推断,当函数的参数少于三个的时候,通过eax,ecx,edx来传递,当大于三个超过的部分通过栈来传递,而且是值传递(限于32bit Intel CPU)。