Linux内存管理实践-打印内存区域

2010年11月13日 由 edsionte 5 条评论 »

本文将通过一些简单的内核模块程序,显示一个进程的所有内存区域。通过此程序理解进程的整个地址空间与内存区域之间的关系。

打印内存区域

上文中,我们通过打印某个进程的maps文件来查看某个进程的内存区域。如果你理解了进程,进程的用户空间,内存区域三者之间的关系,那么就可以通过内核模块的方式打印指定进程的内存区域。

通过在内核模块加载函数中调用下述函数,来打印当前进程的内存区域。首先通过全局变量current获得当前进程的mm字段,该字段指向当前进程的用户空间(mm_struct);由于多个内存区域(vm_area_struct)是通过一个双链表(最新内核中)链接在一起的,所以在接下来的for循环当中,依次遍历各个内存区域,打印当前内存区域的起始地址和终止地址,并且打印内核对该区域的操作权限。完整代码在这里

static void list_myvma(void)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma;

	printk("list vma..\n");
        //print the current process's name and pid
	printk("current:%s pid:%d\n",current->comm,current->pid);

	down_read(&mm->mmap_sem);
	//vma is a linklist
	for(vma = mm->mmap; vma; vma = vma->vm_next)
	{
		//from the begining to the ending of a virtual memory area
		printk("0x%lx-0x%lx ",vma->vm_start,vma->vm_end);
		//check the flags of this VMA
		if(vma->vm_flags & VM_READ)
			printk("r");
		else
			printk("-");

		if(vma->vm_flags & VM_WRITE)
			printk("w");
		else
			printk("-");

		if(vma->vm_flags & VM_EXEC)
			printk("x");
		else
			printk("-");

		if(vma->vm_flags & VM_SHARED)
			printk("s");
		else
			printk("p");

	        printk("\n");

	}
	up_read(&mm->mmap_sem);
}

试一下吧!

进程用户空间的代码描述

2010年11月9日 由 edsionte 没有评论 »

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

上文中提到,每一个进程都拥有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年11月8日 由 edsionte 4 条评论 »

我们知道,在x86体系结构中分段机制是必选的,而分页机制则可由具体的操作系统而选择,Linux通过让段的基地址为0而巧妙的绕过了基地址。因此,对于Linux来说,虚地址和线性地址是一致的。在32位的平台上,线性地址的大小为固定的4GB。并且,由于采用了保护机制,Linux内核将这4GB分为两部分,虚地址较高的1GB(0xC0000000到0xFFFFFFFF)为共享的内核空间;而较低的3GB(0x00000000到0xBFFFFFFF)为每个进程的用户空间。由于每个进程都不能直接访问内核空间,而是通过系统调用间接进入内核,因此,所有的进程都共享内核空间。而每个进程都拥有各自的用户空间,各个进程之间不能互相访问彼此的用户空间。因此,对于每一个具体的进程而言,都拥有4GB的虚拟地址空间。

地址映射

一个程序在经过编译、连接之后形成的地址空间是一个虚拟的地址空间,只有当程序运行的时候才会分配具体的物理空间。由此我们可以得知,程序的虚拟地址相对来说是固定的,而物理地址则随着每一次程序的运行而有所不同。

对于内核空间而言,它与物理内存之间存在一个简单的线性关系,即存在3GB的偏移量。在Linux内核中,这个偏移量叫做PAGE_OFFSET。如果内核的某个物理地址为x,那么对应的内核虚地址就为x+PAGE_OFFSET。

对于用户空间而言,它与物理内存之间的映射远不止这么简单。与内核空间和物理空间的线性映射不同的是,分页机制将虚拟用户空间和物理地址空间分成大小相同的页,然后再通过页表将虚拟页和物理页块映射起来。

虚拟地址空间举例

1.内核空间

一般可以通过__get_free_page()、kmalloc()和vmalloc()来申请内核空间。只不过__get_free_page函数每次申请的都是完整的页;而后两者则依据具体参数申请以字节为单位的内存空间。此外,前两个函数申请的虚拟地址空间和物理地址空间都是连续的;vmalloc函数申请的物理地址空间并不连续。vmalloc函数通过重新建立虚拟地址空间和物理地址空间之间的映射,即新建页表项,将离散的物理地址空间映射到连续的虚拟地址空间。因此,使用该函数的开销比较大。

下面的程序简单的演示了这三个函数的使用方法。从结果中可以看出,这些函数申请的地址都在3GB(0xBFFFFFFF)以上。完整代码在这里

static int __init mmshow_init(void)
{
	printk("mmshow module is working\n");

	pagemem = __get_free_page(GFP_KERNEL);
	if(!pagemem)
		goto gfp_fail;
	printk(KERN_INFO "pagemem = 0x%lx\n",pagemem);

	kmallocmem = kmalloc(100 * sizeof(char),GFP_KERNEL);
	if(!kmallocmem)
		goto kmalloc_fail;
	printk(KERN_INFO "kmallocmem = 0x%p\n",kmallocmem);

	vmallocmem = vmalloc(1000000 * sizeof(char));
	if(!vmallocmem)
		goto vmalloc_fail;
	printk(KERN_INFO "vmallocmem = 0x%p\n",vmallocmem);

	return 0;

gfp_fail:
	free_page(pagemem);
kmalloc_fail:
	kfree(kmallocmem);
vmalloc_fail:
	vfree(vmallocmem);

	return -1;
}
//运行结果:
[ 5542.073900] mmshow module is working
[ 5542.073904] pagemem = 0xf3211000
[ 5542.073907] kmallocmem = 0xd581e700
[ 5542.073983] vmallocmem = 0xf9251000

2.用户空间

如前所述,每个进程够拥有属于自己的3GB的虚拟空间,那么这个3GB的空间是如何划分的?通常,除了我们熟悉的代码段和数据段,用户空间还包括堆栈段和堆。我们可以通过下面的演示程序来了解这些区域到底负责存储程序的那些内容。

int bss_var;
int data_var0 = 1;

int main(int argc,char **argv)
{
	printf("The user space's address division of a process as follow:\n");
	printf("Data segment:\n");
	printf("address of \"main\" function:%p\n\n",main);

        printf("Data segment:\n");
	printf("address of data_var:%p\n",&data_var0);
	static int data_var1 = 4;
	printf("new end of data_var:%p\n\n",&data_var1);

        printf("BSS:\n");
	printf("address of bss_var:%p\n\n",&bss_var);

	char *str = (char *)malloc(sizeof(char)*10);
	printf("initial heap end:%p\n",str);
	char *buf = (char *)malloc(sizeof(char)*10);
	printf("new heap end:%p\n\n",buf);

        int stack_var0 = 2;
	printf("Stack segment:\n");
	printf("initial end of stack:%p\n",&stack_var0);
	int stack_var1 = 3;
	printf("new end of stack:%p\n",&stack_var1);

	return 0;
}
//运行结果:
The user space's address division of a process as follow:
Data segment:
address of "main" function:0x8048454
Data segment:
address of data_var:0x804a01c
new end of data_var:0x804a020
BSS:
address of bss_var:0x804a02c
initial heap end:0x8f77008
new heap end:0x8f77018
Stack segment:
initial end of stack:0xbfe0a3b4
new end of stack:0xbfe0a3b0

可以看到,代码段存放程序的代码;数据段存放全局变量和static类型的局部变量。此外,未初始化的全局变量虽然也存在于数据段,但是这些未初始化的变量都集中在靠近数据段上边界的区域,这个区域称为BSS段。以上这些空间是进程所必须拥有的,它们在进程运行之前就分配好了。

程序中的局部变量一般被分配在堆栈段,其位于用户空间最顶部。与固定的代码段和数据段不同的是,堆栈段存储数据是从高低值往低地址延伸的。因此,在数据段到堆栈段之间,形成了一片空洞,这片空洞用于存储malloc函数所动态分配的空间,这片空洞区域被称为堆。

通过下面这个图可以更进一步的了解到进程用户空间的划分情况。

以上是关于进程用户空间划分的大致分析,上述理论在内核代码中如何体现?它将涉及到mm_struct结构和vm_area_struct结构。下文中,将会对这两个结构有详细分析。

指向一维数组的指针

2010年10月30日 由 edsionte 3 条评论 »

今天在看到typedef int (*int_arry)[10];这条语句时,因为对这样的定义使用较少,就想着编写一个test.c来试试看。不过,当我编写完一个简单的测试程序时,却发现我对指向一维数组的指针的使用了解甚少。

起初,我的程序是这样:

#include  < stdio.h >
typedef int (*int_array)[10];
int main()
{

	int a[10]={1,2,3,4,5};
	int_array i=&a;

	printf("%d=%d\n",i[4],a[4]);
      return 0;
}

编译后,提示如下错误:

test.c: In function ‘main’:
test.c:10: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int *’

也就是说,i[4]是一个int*型的指针。为什么会出现这样的错误?既然i是一个指向有10个整形元素数组的指针。那么将i指向数组a,然后使用i[4]获取第四个元素有什么错?

那我们从另一个角度来分析,一般i[4]这样的形式都可以看成*(i+4)这样的形式。i+4是什么?对了!i是一个数组指针,那么i+4也就是一个数组指针。如果将i所指的数组看作一个二维表的第1行,那么i+4就是指向第5行的指针。也就是说它相对于i所指向位置的偏移量为4*sizeof(int)*10个字节。因此*(i+4)仍然是一个指针,只不过它指向第5行的首个元素。

看来我们找到问题所在,i[4]并不是一个整形元素,而是一个指向整形元素的指针。上面程序中,我原本的意思是通过i来打印数组a中第四个元素。那么此刻我们应该这么修改:

printf("%d=%d\n",i[0][4],a[4]);

或者下面任意一句:

printf("%d=%d\n",(*(i+0))[4],a[4]);
printf("%d=%d\n",*(i[0]+4),a[4]);
printf("%d=%d\n",*(*(i+0)+4),a[4]);

你会发现,如果p是一个指向指针的指针,那么总能够通过两次的*、两次的[]或者一次*和一次[]取得这个指针最终指向的数据,因为说到底[]总能够化成*的形式。理解了这些,上面的语句对你也就不成问题了。got it?

支持阻塞操作的字符设备驱动

2010年10月27日 由 edsionte 11 条评论 »

前文中,我们已经知道了如何编写一个简单的字符设备驱动。本文将在此基础上为这个字符设备驱动增加阻塞功能。不过在此之前,我们会先做一些准备工作。

阻塞和非阻塞I/O

阻塞和非阻塞I/O是设备访问内核的两种不同的模式。进程以阻塞方式访问设备并对其进行操作时,如果不能及时获得I/O资源则会被挂起,直到满足可操作的条件后才进行相应的操作。这个被挂起的进程会进入睡眠状态,并被移至某个等待队列;当条件满足时候,会被移出这个等待队列。这里所说的等待队列以及相关操作在上文已说明,在此不再赘述。非阻塞方式是指进程在不能进行设备操作时并不被挂起,它要么放弃操作,要么不停进行查询,直至可以进行相关的设备操作为止。

我们现在已经知道,用户空间中的应用程序通过read()和write()等统一的系统调用来访问设备文件。而这些系统调用函数最终则会调用设备驱动中的XXX_read()和XXX_write()函数。因此,如果我们在设备驱动中实现了阻塞功能(具体会落实到某个操作函数),当应用程序进程不能及时获得设备资源时就会将该进程阻塞到资源可访问为止。那么XXX_read()和XXX_write()等函数也就不会立即返回,read()和write()等系统调用也就不会立即返回。整个阻塞-唤醒的过程用户是无法感知到的。从用户的角度来看,他们会认为直接就可以对此设备进行操作。

相反,如果设备驱动中的操作函数是非阻塞的,那么当设备资源不可用时,设备驱动中的XXX_read()和XXX_write()等函数会立即返回,那么read()和write()等系统调用也会立即返回。从用户角度来看,此时访问设备文件就出错了。

支持阻塞操作的字符设备驱动

接下来要分析的这个字符设备驱动同样使用一个全局的字符串数组globalmem来存储字符数据。XXX_read()负责将内核空间的数据(此处即为globalmem中的字符串)拷贝到用户空间,实现用户空间对设备文件的读操作;XXX_write()负责将用户空间的数据拷贝到内核空间,实现用户空间对该设备文件的写操作。另外,为了更好的演示本文所述的阻塞操作,我们对这个字符串数组globalmem进行这样的限制:当它为空时,读进程不能进行读操作;当它为满的时候,写进程不能进行写操作。当读了count字节的数据后,还要将globalmem中这些被读的数据移出这个全局数组。

如果你理解了前面那个最基本的字符设备驱动的话,除了上述的不同外,基本上没有什么地方你看不懂的。这个举例的完整代码在这里

static char globalmem[BUF_NUM];
static wait_queue_head_t rdwait;
static wait_queue_head_t wrwait;
static struct semaphore mutex;

static int len;
ssize_t myblock_read(struct file*,char*,size_t count,loff_t*);
ssize_t myblock_write(struct file*,char*,size_t count,loff_t*);

struct file_operations fops=
{
	.read=myblock_read,
	.write=myblock_write,
};

static int __init myblock_init(void)
{
	int ret;

	printk("myblock module is working..\n");

	ret=register_chrdev(MAJOR_NUM,"edsionte_block_cdev",&fops);
	if(ret<0)
	{
		printk("register failed..\n");
		return 0;
	}
	else
	{
		printk("register success..\n");
	}
	init_MUTEX(&mutex);
	init_waitqueue_head(&rdwait);
	init_waitqueue_head(&wrwait);

	return 0;
}

在内核模块加载函数中,先申请字符设备号;再初始化互斥信号量mutex;最后分别初始化了读等待队列头和写等待队列头。另外定义了一个全局变量len来记录当前globalmem中实际的字节数,而BUF_NUM则是最大长度。

在读函数中,我们先创建一个代表当前进程的等待队列结点wait,并把它加入到读等待队列当中。但这并不意味着当前进程就已经完全睡眠了,还需要调度函数的调度。我们前面已经说过,当共享数据区的数据长度为0时,就应该阻塞该进程。因此,在循环中,首先将当前进程的状态设置TASK_INTERRUPTIBLE。然后利用schedule函数进行重新调度,此时,读进程才会真正的睡眠,直至被写进程唤醒。在睡眠途中,如果用户给读进程发送了信号,那么也会唤醒睡眠的进程。

当共享数据区有数据时,会将count字节的数据拷贝到用户空间,并且唤醒正在睡眠的写进程。当上述工作完成后,会将当前进程从读等待队列中移除,并且将当前进程的状态设置为TASK_RUNNING。

关于从全局缓冲区移出已读数据,这里要特别说明一下。这里利用了memcpy函数将以(globalmem+count)开始的(len-count)字节的数据移动到缓冲区最开始的地方。

另外,在上述操作过程中,还加入了互斥信号量防止多个进程同时访问共享数据len和globalmem。

ssize_t myblock_read(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&rdwait,&wait);

	while(len==0)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

		down(&mutex);
	}

	if(count>len)
	{
		count=len;
	}

	if(copy_to_user(buf,globalmem,count)==0)
	{
		memcpy(globalmem,globalmem+count,len-count);
		len-=count;
		printk("read %d bytes\n",count);
		wake_up_interruptible(&wrwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto copy_err_out;
	}

copy_err_out:up(&mutex);
signal_out:remove_wait_queue(&rdwait,&wait);

	set_current_state(TASK_RUNNING);
	return ret;
}

在写函数中,如果检测到globalmem当前的长度是BUF_NUM,则阻塞当前的进程;否则,从用户空间将数据拷贝到内核空间。写函数的控制流程大致与读函数相同,只不过对应的等待队列是写等待队列。

ssize_t myblock_write(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&wrwait,&wait);

	while(len==BUF_NUM)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

         	down(&mutex);
	}
	if(count>(BUF_NUM-len))
	{
		count=BUF_NUM-len;
	}

	if(copy_from_user(globalmem+len,buf,count)==0)
	{
		len=len+count;
		printk("written %d bytes\n",count);
		wake_up_interruptible(&rdwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto COPY_ERR_OUT;
	}

signal_out:up(&mutex);
COPY_ERR_OUT:remove_wait_queue(&wrwait,&wait);
	set_current_state(TASK_RUNNING);

	return ret;
}

上述就是支持阻塞模式的字符设备驱动。关于上述程序更多的解释如下:

1.两种睡眠。当读进程读数据时,如果发现写进程正在访问临界区,那么它会因为不能获得互斥信号量而阻塞;而当读进程获得信号量后,如果当前globalfifo的数据数为0,则会阻塞。这种阻塞是由我们在设备驱动中实现的。

2.两种唤醒。当写进程离开临界区并释放信号量时,读进程会因信号量被释放而唤醒;当写进程往globalfifo中写入了数据时,读进程会被写进程中的唤醒函数所唤醒。特别的,如果读进程是以轻度睡眠方式睡眠的,那么用户可以通过发送信号而唤醒睡眠的读进程。

3.唤醒后如何执行。无论因哪种方式而睡眠,当读进程被唤醒后,均顺序执行接下来的代码。

4.down操作和add_wait_queue操作交换。在原程序中,读进程先获取信号量,再将读进程对应的等待队列项添加到读等待队列中。如果交换,当读进程的等待队列项加入到等待队列后,它可能又会因未获得信号量而阻塞。

5.up操作和remove_wait_queue操作交换。这两个操作分别对应标号out和out2。如果读进程从内核空间向用户空间拷贝数据失败时,就会跳转到out。因为读进程是在获得信号量后才拷贝数据的,因此必须先释放信号量,再将读进程对应的等待队列项移出读等待队列。而当读进程因信号而被唤醒时,则直接跳转到out2。此时读进程并没有获得信号量,因此只需要移出队列操作即可。如果交换上述两个操作,读进程移出等待队列时还未释放互斥信号量,那么写进程就不能写。而当读进程因信号而唤醒时,读进程并没有获得信号量,却还要释放信号量。

通过下述方法,你就可以体验到以阻塞方式访问设备文件。

1.make编译文件,并插入到内核;
2.创建设备文件结点:sudo mknod /dev/blockcdev c major_num 0;
3.修改设备文件权限:sudo chmod 777 /dev/blockcdev;
4.终端输入:cat /dev/blockcdev&;即从字符设备文件中读数据,并让这个读进程在后台执行,可通过ps命令查看到这个进程;
5.中断继续输入:echo ‘I like eating..’ > /dev/blockcdev;即向字符设备文件中写入数据;

通过上述的步骤,可以看到,每当利用echo命令写入数据时,后台运行的读进程就会读出数据,否则读进程一直阻塞。此外,如果你愿意的话,还可以分别编写一个读写进程的程序进行测试。

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