日志标签 ‘结构体’

进程用户空间的代码描述

2010年11月9日

在前文中,我们对进程的虚拟地址空间进行了概述。本文将从内核代码的角度来分析进程的用户空间的的组织和结构。

上文中提到,每一个进程都拥有3GB大小的用户空间,而连续用户空间又按照存储内容的不同被划分成若干个区域。在内核中,主要通过mm_struct结构体和vm_area_struct结构体对进程用户空间进行描述。前者是对进程的用户空间进行整体的描述;而后者则是对用户空间中的某个区域进行描述。显然,每一个进程对应的有一个mm_struct结构和多个vm_area_struct结构。

1.mm_struct结构

最新版本中的mm_struct结构字段比较多,接下来只对部分字段做以说明。

mmap:vm_area_struct结构体类型的指针。指向进程用户空间中各区域所组成的双链表。链表方式可以高效的遍历所有元素;
mm_rb:rb_root结构体类型。同样描述内存区域块,只不过采用红黑树来表示。用红黑树可以快速索引到指定的元素;
mm_users:atomic_t类型。用来记录正在使用该地址空间的进程数目。比如,当前有3个进程正在共享该地址空间,那么其值为3;
mm_count:atomic_t类型。记录mm_struct结构体被引用的次数。如果当前该地址空间只被两个进程所共享,那么该值为1,mm_users为2;当这两个进程都退出时,该值为0,mm_users也为0。另外,内核线程并不需要访问用户的内存空间,也并不需要创建页表。内核线程一般会直接使用前一个进程的mm_struct结构。因此该字段的计数还包括内核线程对这个结构的引用。
map_count:int类型。内存区域的个数;
pgd:pgd_t类型,该结构体类型内部封装的是unsigned long类型的数据。pgd表示的是页目录基址。当调度程序调度一个进程运行时,就将这个线性地址转化为物理地址,并写入CR3控制寄存器中;
start_code, end_code, start_data, end_data:unsigned long类型。进程代码段和数据段的起始地址和终止地址;
start_brk, brk, start_stack:unsigned long类型。分别为堆的起始地址和终止地址,堆栈的起始地址。上文说过,进程的堆栈段是根据需求向下(朝低地址方向)延伸的,因此这里并没有堆栈段的终止地址;
arg_start, arg_end, env_start, env_end:unsigned long类型。命令行参数所在内存的起始地址和终止地址,环境变量所在内存的起始地址和终止地址;

2.vm_area_struct结构

上面我们已经知道,该结构体描述的是进程用户空间中的一个虚拟内存区间(Virtual Memory Area,VMA)。
vm_mm:mm_struct结构体类型指针。指向该区域所属的用户空间对应的mm_struct结构体。
vm_start,vm_end:unsigned long类型。该虚存区域的起始地址和终止地址。
vm_next,vm_prev:vm_area_struct结构体类型指针。构成VMA双联表。
vm_flags:unsigned long类型。该虚存区的标志。
vm_page_prot:pgprot_t结构体类型,内部封装了unsigned long类型。访问控制权限。
vm_ops:vm_operations_struct结构体类型。该虚存区域的操作函数接口,这些函数可以对虚存区中的页进行操作。

3.数据结构的关系

了解了上述结构体的关键字段,它们与进程之间的逻辑关系便是我们接下来要关心的重点。我们知道,一个进程在内核中使用task_struct结构对其进行描述。task_struct结构中有一个mm字段,它所指向的便是与该进程用户空间所对应的mm_struct结构体。通过上述分析,我们知道mm_struct结构中有mmap字段,它指向VMA双链表。因此,我们使用current->mm->mmap就可以获得VMA链表的头指针。那么current->mm->mmap->vm->next就可以获得指向该VMA双联表的下一个结点的指针。

4.动手查看内存区域

上述我们从代码角度分析了用户地址空间和内存区域。那么对于一个任意的进程,我们如何查看它的内存空间和所划分的内存区域?

我们先看一个简单的测试程序:

#include 
#include 

int main()
{
	int i=1;
	char *str=NULL;
	printf("hello,world!\n");
	str=(char *)malloc(sizeof(char)*1119);

	sleep(1000);

	return 0;
}

这个程序中使用到了malloc函数,因此str变量存储于堆中。我们通过打印/proc/3530/maps文件,即可看到该进程的内存空间划分。其中3530是该进程的id。

edsionte@edsionte-desktop:~$ cat /proc/3530/maps
0014a000-00165000 r-xp 00000000 08:07 398276     /lib/ld-2.11.1.so
00165000-00166000 r--p 0001a000 08:07 398276     /lib/ld-2.11.1.so
00166000-00167000 rw-p 0001b000 08:07 398276     /lib/ld-2.11.1.so
001d8000-0032b000 r-xp 00000000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032b000-0032c000 ---p 00153000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032c000-0032e000 r--p 00153000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032e000-0032f000 rw-p 00155000 08:07 421931     /lib/tls/i686/cmov/libc-2.11.1.so
0032f000-00332000 rw-p 00000000 00:00 0
00441000-00442000 r-xp 00000000 00:00 0          [vdso]
08048000-08049000 r-xp 00000000 08:09 326401     /home/edsionte/test
08049000-0804a000 r--p 00000000 08:09 326401     /home/edsionte/test
0804a000-0804b000 rw-p 00001000 08:09 326401     /home/edsionte/test
08958000-08979000 rw-p 00000000 00:00 0          [heap]
b78ce000-b78cf000 rw-p 00000000 00:00 0
b78dd000-b78e0000 rw-p 00000000 00:00 0
bfa6a000-bfa7f000 rw-p 00000000 00:00 0          [stack]

每一行信息依次显示的内容为内存区域其实地址-终止地址,访问权限,偏移量,主设备号:次设备号,inode,文件。

上面的信息不但包含了test可执行对象的各内存区域,而且还分别显示了 /lib/ld-2.11.1.so(动态连接程序)文件和/lib/tls/i686/cmov/libc-2.11.1.so(C库)文件的内存区域信息。

我们从某个内存区域的访问权限上可以大致判断该区域的类型。各个属性符号的意义为:r-read,w-write,x-execute,s-shared,p-private。因此,r-x一般代表程序的代码段,即可读,可执行。rw-可能代表数据段,BSS段和堆栈段等,即可读,可写。堆栈段从行信息的文件名就可以区分;如果某行信息的文件名为空,那么可能是BSS段。另外,上述test进程共享了内核动态库,所以在00441000-00442000行处文件名显示为vdso(Virtual Dynamic Shared Object)。

遍历进程链表

2010年10月25日

我们知道,一个进程是由进程控制块(PCB),代码段和数据段组成的;并且,OS通常是通过PCB来感知一个进程的存在。其实PCB就是操作系统对每个进程的代码描述。linux内核中使用task_struct结构来描述一个PCB(具体可以在linux/kernel/sched.c查看源码);多个进程则常常使用双链表等来进行组织。比如可运行状态的进程组成可运行队列,等待状态的进程组成等待队列等。

本文将使用前文中所分析的list_head结构来遍历内核中的进程链表。task_struct结构中使用多个字段来详细的描述一个进程。本文中所属的遍历函数只打印一些常用的信息,即进程名称,进程的pid。

我们利用list.h中的下述遍历宏对整个进程链表进行遍历:

420#define list_for_each_entry(pos, head, member)                          \
421        for (pos = list_entry((head)->next, typeof(*pos), member);      \
422             prefetch(pos->member.next), &pos->member != (head);        \
423             pos = list_entry(pos->member.next, typeof(*pos), member))

先简单了解一下这个宏的使用方法。由于list_head结构中没有数据字段,所以它经常被嵌套在其他的结构体当中。pos指向包含list_struct结构的结构体;head为list_head类型的指针,我们可以使用链表中任意一个结点的指针;member即为list_head类型的变量名。

对应到我们下面的程序,pos为指向task_struct类型的指针,我们通过遍历宏即得到每个进程对应的task_struct类型的指针;我们将current_head赋予遍历宏中的第二个参数,current是一个宏,即为系统内正在运行的进程;由于list_struct结构在task_struct结构中的变量明为tasks,因此我们将tasks传递给便利宏的第三个参数。

#include< linux/init.h >
#include< linux/module.h >
#include< linux/sched.h >
#include< linux/sem.h >
#include< linux/list.h >

static int __init  traverse_init(void)
{
      struct task_struct *pos;
      struct list_head *current_head;
      int count=0;

      printk("Traversal module is working..\n");
      current_head=&(current->tasks);
      list_for_each_entry(pos,current_head,tasks)
      {
             count++;
             printk("[process %d]: %s\'s pid is %d\n",count,pos->comm,pos->pid);
      }
      printk(KERN_ALERT"The number of process is:%d\n",count);
      return 0;
}

了解了便利宏的使用方法,那么理解上述代码就简单的多了。关于便利宏的代码详解可参考这里的文章。

加载函数要做的工作就是这些,那么卸载函数的功能就相对于简单多了。

static void __exit traverse_exit(void)
{
  printk("hello world exit\n");
}

OK,编译,插入内核,再查看内核日志文件吧!

字节对齐

2010年7月23日

今天在看结构体和共用体部分的时候,遇到了一个新名词“内存对齐”。先引入问题吧。如下:

struct student
{
	char name[20];
	int age;
	char sex;
	char phone[15];
};
struct student p1;

sizeof(p1)=?
这个很简单得出答案,即20+4+1+15=40Byte。如果将phone[15]改为phone[16],结果是44。难道不是41吗?

这里便要引入内存对齐的概念。内存为了提高访问效率,便规定以结构体中最大的基本单位长度为对齐标准。即实际分配的内存大小是对齐标准的整数倍(必要条件)。在上面的结构体中,最大的基本类型是int。因此以4Byte为对其标准。所以实际内存大小应该为4的整数倍,即为44Byte.

也许你有疑惑:name[20]不是要20Byte吗,为什么以4Byte为对齐标准?请注意这里的基本类型。name[20]是一个字符串数组,数组属于复合的数据类型。复合的数据类型还有结构体,共用体。基本的数据类型是整型,字符型,浮点型。如果你还有不解,那么看下题:

struct score
{
	float english;
	float math;
	float computer;
};
struct student
{
	char name[10];
	int age;
	char sex;
	struct score st_score;
};

在student结构体中含有数据类型为struct score这样的变量。struct score的大小为12Byte,也是struct student结构体中最大的数据类型,但是我们的对齐标准是4Byte,就如我们刚说的那样,内存的对齐标准是取结构体中最大的基本数据类型,这里我们取sizeof(int)。

再看下面的问题:

union data1
{
	int i;
	char c;
	char str[9];
};
struct data2
{
	int i;
	char c;
	char str[9];
};

sizeof(struct data1)=? sizeof(struct data2)=?
对于前者,由于共用体的存储大小由最大的成员来决定,因此上题中共用体的存储大小为9Byte,考虑到内存对齐,它以sizeof(int)=4Byte来对齐,因此实际内存分配大小是12Byte。对于后者,存储大小是4+1+9=14Byte,这个结构体以4对齐,因此实际分配大小为16Byte。

现在你应该对于共用体和结构体的内存对齐有所了解了。那么再接着往下看:

struct data
{
	char c;
	double d;
	char ch;
}

sizeof(struct data)=?
有了上面的理解,10肯定不会是答案,那么会是16吧。也错。正确答案是24。你可能会感到莫名其妙:按照上面所说的方法,以8为对齐标准,那么10不是8的倍数,那就是16呀。为什么呢?请你耐心看下面。

首先我们知道结构体变量是分配的连续内存空间。d毫无疑问是分配8Byte,对于d“前面”的c,按照对齐标准我们应该分配8Byte,而d后面的ch,按照对齐标准也应该分配8Byte。因此结果是24。

如果这样:

struct data
{
	double d;
	char c;
	char ch;
}

sizeof(struct data)=16。

因为内存没必要分别为c和ch分配8Byte来进行对齐,将它们放在一个8byte里就可以实现内存对齐。上面在引入内存对齐概念的时候,我用括号特别注释必要条件便是说明此处内容。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key