解剖linux内核之进程

本文详细介绍了Linux内核中的进程管理,包括如何通过内核结构体`struct task_struct`遍历所有进程,使用systemtap脚本语言打印进程信息,以及探讨了namespace的概念并提供了验证其功能的实验。此外,还展示了通过systemtap监测进程切换频率的方法,并讨论了内核函数参数传递的方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

      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)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值