TCP并发服务器程序设计

8 6 月, 2011 by edsionte 无评论 »

TCP客户/服务器程序设计模型-并发服务器

上文中所说的迭代服务器简单易懂,将它作为学习socket编程的例子再适合不过了。但是在一般的实际应用中我们并不会使用这个简单的迭代服务器,因为这样的服务器每次只能服务一个客户,如果这个客户请求长时间占用服务器,那么其他客户将一直不能被服务。较为理想的情况是服务器可以同时为多个客户服务,接下来将要说明的并发服务器就可以满足这样的要求。

1.并发服务器原理

关于并发服务器的基本原理可以参考本博客之前的文章,这里不在赘述。下面将通过一个具有并发服务器的C/S模型示例来说明并发服务器程序设计的细节。

2客户程序设计

客户程序与上文所述的客户程序类似,只不过在客户端进行connect之后,客户端与服务器之间的所有数据交互都封装在了str_cli函数中。客户程序主函数在调用该函数时不仅要向该函数传递套接字,还要传递标准输入文件指针stdin。具体代码如下:

void str_cli(FILE *fp, int sockfd)
{
	char sendline[DATA_MAX], recvline[DATA_MAX];

	memset(sendline, 0, DATA_MAX);
	memset(recvline, 0, DATA_MAX);

	while(fgets(sendline, DATA_MAX, fp) != NULL) {
		if (write(sockfd, sendline, strlen(sendline)) < 0) {
			printf("write error!\n");
			exit(1);
		}

		if (read(sockfd, recvline, DATA_MAX) == 0) {
			printf("read error!\n");
			exit(1);
		}
		fputs(recvline, stdout);

          	memset(sendline, 0, DATA_MAX);
         	memset(recvline, 0, DATA_MAX);
	}
}

从客户端的数据交互函数str_cli中可以看出客户端进行的操作如下:

1.客户程序通过fgets从标准输入文件读入数据,并通过write将数据发送至服务器端;

2.客户程序通过read函数读取服务器发来的数据,并通过fputs将数据写入标准输出文件;

3.如果fgets读入数据错误时,整个循环结束,客户端也将结束;

3.服务器程序设计

并发服务器的经典模型在本博客之前的文章中有所提到,这里只是特别说明服务器数据处理函数。

void str_echo(int sockfd)
{
	int n;
	char buf[DATA_MAX];

again:
	while((n = read(sockfd, buf, DATA_MAX)) > 0)
		if (write(sockfd, buf, n) < 0) {
			printf("write error!\n");
			exit(1);
		}

	if (n < 0 && errno == EINTR)
		goto again;
	else if (n < 0)
		printf("str_echo error!\n");
}

服务器程序完成的工作如下:

1.通过read读取客户程序发送的数据;

2.通过write将刚刚读到的数据重新发送给客户程序;

3.如果客户端正常退出,那么服务器的read函数将返回0并退出循环;否则将显示错误信息;

通过上述对客户和服务器程序的描述,我们可以得知该C/S模型的工作方式是:客户程序从标准输入读入用户输入的数据,并发送至服务器;服务器从网络输入读入该数据,立即将读入的数据发送给客户程序;客户从网络输入读取服务器发来的数据,并将其显示在标准输出上。

也就是说,每当用户在客户端输入数据,客户端在终端上又会立即回显用户所输入的数据。因此,我们将上述所举例的具有并发服务器的C/S模型称之为回射客户/服务器程序。

TCP迭代服务器程序设计

7 6 月, 2011 by edsionte 无评论 »

TCP客户/服务器程序设计模型-迭代服务器

本博客将从本文开始持续更新基于TCP的C/S程序设计基本模型系列文章。一开始我们以一个简单的C/S模型为例进行学习,在后续的文章中我们将给出不同的客户/服务器模型。

下面将要示例的程序具有这样的功能:客户程序与服务器建立一个TCP连接后,服务器以友好的方式将当前的时间和日期发送给客户程序。我们接下来重点对示例程序的设计模式和一些设计细节作以说明,程序中所涉及的socket接口具体可以参考这里的文章。

1.客户程序设计

在基于socket的C/S模型中,客户程序往往比服务器程序简单的多。客户程序通常完成的工作是:通过socket函数创建一个套接字;connect到服务器;如果连接成功,则与服务器进行数据交互,具体为从服务器读取数据或向服务器写入数据。客户程序的核心代码如下:

int main(int argc, char **argv)
{
	int sockfd, n;
	char recvline[MAXLINE + 1];
	struct sockaddr_in serv_addr;

	if (argc != 2) {
		printf("Usage:a.out \n");
		exit(1);
	}

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		my_error("socket", errno, __LINE__);

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(4555);

	if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
 		my_error("inet_pton", errno, __LINE__);
 	if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
  		my_error("connect", errno, __LINE__);
 	while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
		recvline[n] = 0;
		if (fputs(recvline, stdout) == EOF)
			my_error("fputs", errno, __LINE__);
	}

	if (n < 0)
		printf("Error:read error\n");

	exit(0);
}

关于客户端程序的设计,说明如下:

1.客户端程序每次启动时,以命令行参数形式将服务器IP传递给客户端。因此程序一开始必须检测命令行参数的合理性;

2.由于我们的C/S模型是基于TCP的字节流套接字,因此相应的对socket函数使用AF_INET和SOCK_STREAM参数。socket函数返回一个整形的套接字描述符,在客户程序中要使用的connet和read等函数将依靠此描述符来对套接字进行操作;

3.在客户连接至服务器之前前,必须设置网际套接字地址结构,即依次对sockaddr_in结构中的字段填入正确的值。htons函数将主机字节序列转换成网络字节序列;inet_pton函数则是将字符形式的IP地址转换成二进制数值形式;

4.connect将与其第二个参数所指定的服务器建立一个TCP连接。connect函数的第二个参数为通用套接字地址结构sockaddr,因此必须对网际套接字地址结构进行强制类型转换;

5.通过套接字描述符sockfd从服务器端进行读数据,并通过fputs函数将读到的数据显示在标准输出设备;

2.服务器程序设计

服务器程序按照以下步骤进行设计:socket一个套接字;将服务器端口bind到服务器套接字上;通过listen函数将服务器套接字转换成监听套接字;accept客户端的连接;如果连接成功,则与客户端进行数据交互;终止连接。服务器程序核心程序如下所示。

int main(int argc, char **argv)
{
	int listenfd, connfd;
	struct sockaddr_in serv_addr, client_addr;
	char buf[MAXLINE];
	time_t ticks;
	int addr_size;

	if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		my_error("socket", errno, __LINE__);

	memset(&serv_addr, 0, sizeof(struct sockaddr_in));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(4555);

	if (bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
		my_error("bind", errno, __LINE__);

	if (listen(listenfd, LISTENQ) == -1)
		my_error("listen", errno, __LINE__);

	for (;;) {
		addr_size = sizeof(struct sockaddr);
		if ((connfd = accept(listenfd, (struct sockaddr*)&client_addr, &addr_size)) == -1)
			my_error("accept", errno, __LINE__);

		printf("%s has connectd to server..\n", inet_ntoa(client_addr.sin_addr));
		ticks = time(NULL);
		snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
		if (write(connfd, buf, strlen(buf)) < 0)
			my_error("write", errno, __LINE__);

		close(connfd);
	}
}

关于服务器程序的相关细节,说明如下:

1.在设置网际套接字地址时,将IP地址指定为INADDR_ANY。如果服务器主机有多个网络接口,那么服务器进程就可以在任意网络接口上接受客户连接;

2.listen函数将套接字转换成监听套接字,这样内核就可以通过该套接字接受客户端的外来连接请求;

3.socket、bind和listen这三步是TCP服务器获得监听套接字描述符的通用步骤;

4.服务器的连续运转是通过死循环来实现的,并通过accept函数使得服务器程序不会过于占据系统内存。通常服务器进程通过accept函数进入阻塞状态,直到某个客户连接到达时才会被唤醒。客户和服务器之间的TCP连接是通过三次握手来建立的,accept函数建立连接成功后会返回另一个套接字描述符——已连接套接字。该套接字用于和刚刚连接的客户进行通信。

5.time函数将返回1970年1月1日至现在的秒数,而ctime函数将这些秒数转换成友好的时间格式;

6.wirte函数通过已连接套接字connfd将时间信息发送至客户端;

通过服务器程序我们可以看到,如果服务器没有处理客户端的请求,那么它将一直处于阻塞状态;如果服务器正在处理来自客户端的连接请求,那么其他请求连接至服务器的客户进程则一直处于阻塞状态。只有当服务器处理完当前客户的连接请求时,其他客户的连接请求才有机会被服务器处理。我们将此服务器称之为迭代式服务器,也就是说服务器对于每个客户都将迭代执行一次。

上述所示例的迭代服务器适用性很差,因为在服务器处理当前的客户请求之前,其余等待的客户程序都不能被处理。但它作为一个基本的C/S模型,对于初学者以及理解后续C/S模型都有极大帮助。

内核模块的实现

29 5 月, 2011 by edsionte 无评论 »

内核模块再学习之模块的实现

如果你对内核模块编程已经有了简单的认识,那么可以更进一步学习模块在内核中的实现。对于每个内核模块来说,系统都为其分配一块内存区,这块内存区包括:一个module结构、唯一表示模块名称的字符串和实现模块功能的代码。

module结构中包含众多字段,从多个方面对内核模块进行描述,这一点如同task_struct结构对一个进程的全面描述一样。我们接下来只关注几个与我们平时简单的内核模块编程相关的字段。

enum module_state state:显示内核模块的状态;该枚举类型包含三种模块状态:MODULE_STATE_COMING、MODULE_STATE_LIVE和MODULE_STATE_GOING。分别表示正在加载模块、模块已经加载并且可用、正在卸载模块。
struct list_head list:内核中的所有模块结构通过双链表组织在一起,该字段表示当前模块在双链表中的节点。
char name[MODULE_NAME_LEN]:模块的名称,每个模块的名称都是唯一的。
int (*init)(void):init是一个函数指针,它指向模块的加载函数。
void (*exit)(void):exit是一个函数指针,它指向模块的卸载函数。
unsigned int core_size:用于模块核心函数与数据结构的动态内存区大小。

通过上述的字段分析,我们知道内核中的模块是通过双链表进行组织的。因此,我们就可以通过双链表的操作函数来打印内核中每个模块的相关信息。不过,模块链表的头指针modules并未被导出,所以我们通过在符号表中查看modules在内存中的地址来对其进行操作(关于通过符号表得到内核中某个未导出符号的地址可查看这里的文章)。整个程序的关键代码参考如下。

#define modules 0xc0771264

static int __init print_module_init(void)
{
	struct module *p;
	struct list_head *module_head;
	struct list_head *pos;
	int i = 0;

	printk("print_module module is starting..\n");

	module_head = (struct list_head *)modules;
	list_for_each(pos, module_head) {
		p = list_entry(pos, struct module, list);
		printk("%3d %20s %10d\n", ++i, p->name, p->core_size);
	}

	return 0;
}

遍历的方法很简单,先得到模块链表的头指针module_head,再通过list_for_each函数依次遍历每个内核模块。我们在此打印的模块信息是模块的名称和模块的大小,其输出结果和lsmod命令的输出结果相同。如下所示。

[ 7405.518605] print_module module is starting..
[ 7405.518610]   1         print_module        632
[ 7405.518613]   2                hello        830
[ 7405.518616]   3          binfmt_misc       6587
[ 7405.518618]   4           vboxnetadp       6808
[ 7405.518621]   5           vboxnetflt      18753
[ 7405.518624]   6                  via      37920
[ 7405.518626]   7                  drm     162345
[ 7405.518629]   8              vboxdrv     229952
…………

完整代码可以在这里下载。

使用list_head建立双联表

24 5 月, 2011 by edsionte 2 comments »

内核中与链表有关的操作都集中在list.h文件中,该文件不仅定义了链表数据结构,而且包含大量对链表操作的函数。

下面通过一个内核模块简单说明内核双链表的用法。该内核模块的加载函数首先建立了一个具有N个节点的双链表,然后再遍历该链表;在该内核模块卸载函数中,依次将每个节点从双链表中删除,并释放每个节点所占用的内核空间的内存。

在具体说明该程序之前,有必要搞清楚两个概念:链表中的数据节点和链表节点。内核在list.h中定义的链表节点list_head称之为链表节点。该结构除了指向前后节点的指针外,并不包含任何数据变量。因此,在实际使用的过程中,我们必须将所需数据和链表节点封装在一个数据结构中,这个新的数据结构就是数据节点。比如,在示例程序中,我们的数据节点除了包含链表节点外,还包含一个数据成员num。

#define N 10
//链表节点
struct numlist {
	int num;//数据
	struct list_head list;//指向双联表前后节点的指针
};
struct numlist numhead;//头节点

在使用双联表的操作函数时也应该注意数据节点和链表节点之间的差异。所有与双联表有关的操作函数都是对链表节点的操作,也就是说我们应该对这些函数传入链表节点(或指针)而不是数据节点。

在加载函数中,我们通过INIT_LIST_HEAD宏初始化链表的头指针。通过查看该宏的定义可知,必须向其传入一个list_head结构的指针变量,因此我们将&numhead.list传入其中。

接着,通过循环分别创建N个数据节点,并在创建完每个节点后,将其加入整个双链表中。将节点加入双链表的过程是通过list_add_tail函数完成的,该函数将新节点加入到双链表的末尾。

static int __init doublelist_init(void)
{
	//初始化头节点
	struct numlist *listnode;//每次申请链表节点时所用的指针
	struct list_head *pos;
	struct numlist *p;
	int i;

	printk("doublelist is starting...\n");
	INIT_LIST_HEAD(&numhead.list);

	//建立N个节点,依次加入到链表当中
		for (i = 0; i < N; i++) {
		listnode = (struct numlist *)kmalloc(sizeof(struct numlist), GFP_KERNEL);
		listnode->num = i+1;
		list_add_tail(&listnode->list, &numhead.list);
		printk("Node %d has added to the doublelist...\n", i+1);
	}

加载函数创建完双链表后,紧接着又对该双链表进行了遍历操作。我们在上面说过双链表的操作函数是对链表节点list_head进行操作的,因此遍历宏list_for_each也是对链表节点的遍历。如果我们需要得到数据节点中每个成员的值,那么就必须获得当前数据节点的指针,也就是该节点的首地址。因此list_entry宏的作用就是通过当前正在遍历的链表节点的指针pos获得其所属数据节点的指针p。

这里特别说明一下list_entry宏的参数,pos所指向的list_head结构在numlist结构中的字段称为list。

	//遍历链表
	i = 1;
	list_for_each(pos, &numhead.list) {
		p = list_entry(pos, struct numlist, list);
		printk("Node %d's data:%d\n", i, p->num);
		i++;
	}

	return 0;
}

在卸载函数中将会依次删除链表中的节点。list_for_each_safe遍历宏是专门为删除链表结点而设计的,它在遍历当前节点对同时保存下一个节点的地址。因此在每次遍历的时候一方面使用list_del将当前链表节点从整个链表中删除,一方面使用kfree函数释放该数据节点所占的内存空间。

static void __exit doublelist_exit(void)
{
	struct list_head *pos, *n;
	struct numlist *p;
	int i;

	//依次删除N个节点
	i = 1;
	list_for_each_safe(pos, n, &numhead.list) {
		list_del(pos);//从双联表中删除当前节点
		p = list_entry(pos, struct numlist, list);//得到当前数据节点的首地址,即指针
		kfree(p);//释放该数据节点所占空间
		printk("Node %d has removed from the doublelist...\n", i++);
	}
	printk("doublelist is exiting..\n");
}

该实例程序的完整代码可在此下载。

对/proc文件系统进行读写操作

19 5 月, 2011 by edsionte 2 comments »

本博客之前的文章中多次涉及到/proc文件系统,下面的几条命令都在曾经的文章中出现过:

cat /proc/interrupts
cat /proc/devices
cat /proc/kallsyms | grep super_blocks

第一条命令用于查看系统内已注册的中断信息,包括中断号、已接受的手段请求和驱动器名称等;第二条命令用于查看系统内已注册的字符设备和块设备信息,包括设备号和设备名称;第三条命令用于在内核符号表中检索super_blocks符号的的地址,kallsyms文件包括内核中所有的标示符及其地址。

1.概述

proc即process的缩写,最初的proc文件系统只是存放进程的相关信息。但现在的/proc文件系统除此之外还包含系统的状态信息和配置信息。
通过ls命令就可以查看/proc文件系统所包含的内容。

edsionte@edsionte-desktop:/proc$ ls
1      1290   1469  1541  1627   19612  29    49    9          dri              mdstat          sys
10     13     1471  1544  1630   19613  3     5     908        driver           meminfo         sysrq-trigger
1013   1301   1474  1548  1632   19629  30    50    913        edsionte_procfs  misc            sysvipc
…………

其中以数字为名的目录即为系统中正在运行的进程信息,数字即为进程的pid。比如我们可以进入init进程的目录,查看它的地址空间:

edsionte@edsionte-desktop:/proc/1$ sudo cat maps
[sudo] password for edsionte:
00110000-00263000 r-xp 00000000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00263000-00264000 ---p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00264000-00266000 r--p 00153000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00266000-00267000 rw-p 00155000 08:07 704702     /lib/tls/i686/cmov/libc-2.11.1.so
00267000-0026a000 rw-p 00000000 00:00 0
0026a000-00272000 r-xp 00000000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00272000-00273000 r--p 00007000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00273000-00274000 rw-p 00008000 08:07 704713     /lib/tls/i686/cmov/libnss_nis-2.11.1.so
00471000-0048b000 r-xp 00000000 08:07 1048610    /sbin/init
…………

除了查看进程的相关信息,我们还可以通过打印相关文件来查看系统的当前运行状态。比如查看当前内存的使用情况:

edsionte@edsionte-desktop:/proc$ cat meminfo
MemTotal:         961368 kB
MemFree:          145264 kB
Buffers:           31648 kB
Cached:           297716 kB
SwapCached:        14436 kB
…………

总之,/proc文件系统相当于内核的一个快照,该目录下的所有信息都是动态的从正在运行的内核中读取。

基于这种原因,/proc文件系统就成为了用户和内核之间交互的接口。一方面,用户可以从/proc文件系统中读取很多内核释放出来的信息;另一方面,内核也可以在恰当的时候从用户那里得到输入信息,从而改变内核的相关状态和配置。

相比传统的文件系统,/proc是一种特殊的文件系统,即虚拟文件系统。这里的虚拟是强调/proc文件系统下的所有文件都存在于内存中而不是磁盘上。也就是说/proc文件系统只占用内存空间,而不占用系统的外存空间。

2.用户态和内核态之间的数据通信

既然内核的数据以/proc文件系统的形式呈现给用户,也就是说内核的信息以文件的形式存在于该文件系统中,那么/proc文件系统就应当提供一组接口对其内的文件进行读写操作。接下来我们以一个实际的内核模块程序easyProc.c为例,说明/proc文件系统的常用接口。该程序中依次创建了几个虚拟文件,然后在用户态对这些文件进行读写测试。

2.0数据结构

每个虚拟文件都对应一个proc_dir_entry类型的数据结构,该结构具体定义如下:

struct proc_dir_entry {
	const char *name;			// virtual file name
	mode_t mode;				// mode permissions
	uid_t uid;				// File's user id
	gid_t gid;				// File's group id
	struct inode_operations *proc_iops;	// Inode operations functions
	struct file_operations *proc_fops;	// File operations functions
	struct proc_dir_entry *parent;		// Parent directory
	...
	read_proc_t *read_proc;			// /proc read function
	write_proc_t *write_proc;		// /proc write function
	void *data;				// Pointer to private data
	atomic_t count;				// use count
	...
};

除了保存该虚拟文件的基本信息外,该结构中还有read_proc和write_proc两个字段,下文中将有详细说明。

2.1创建目录

/proc文件系统中创建一个目录对应的函数接口如下:
struct proc_dir_entry *proc_mkdir(const char *name,struct proc_dir_entry *parent);
其中name为要创建的目录名;parent为这个目录的父目录,当要创建的目录位于/proc下时此参数为空。比如我们使用该函数在/proc下创建一个目录edsionte_procfs。

#define MODULE_NAME "edsionte_procfs"
struct proc_dir_entry *example_dir;
	example_dir = proc_mkdir(MODULE_NAME, NULL);
	if (example_dir == NULL) {
		rv = -ENOMEM;
		goto out;
	}
2.2创建普通文件

在/proc文件系统中创建一个虚拟文件可以使用如下的函数:

 static inline struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_dir_entry *parent) ;

该函数中name为要创建的文件名;mode为创建文件的属性;parent指向该文件父目录的指针,如果创建的虚拟文件位于/proc下,则这个参数为NULL。

比如我们通过该函数在/proc/edsionte_procfs目录下创建一个虚拟文件foo,其权限为644。其中example_dir指向我们刚创建的目录文件edsionte_procfs。

struct proc_dir_entry  *foo_file;
	foo_file = create_proc_entry("foo", 0644, example_dir);
	if (foo_file == NULL) {
		rv = -ENOMEM;
		goto no_foo;
	}
2.3.创建符号链接文件

当我们需要在/proc文件系统下创建一个符号链接文件时,可使用如下接口:

struct proc_dir_entry *proc_symlink(const char *name, struct proc_dir_entry *parent, const char *dest);

name参数为要创建的符号链接文件名;parent为该符号链接文件的父目录;dest为符号链接所指向的目标文件。

下面的代码演示了如何通过该函数来对已存在的虚拟文件jiffies创建符号链接文件jiffies_too:

	symlink = proc_symlink("jiffies_too", example_dir, "jiffies");
	if (symlink == NULL) {
		rv = -ENOMEM;
		goto no_symlink;
	}

我们内核模块加载函数中完成上述几个虚拟文件的创建工作。

2.4.删除文件或目录

既然有创建虚拟文件的函数,必然也就有删除虚拟文件的函数接口:

void remove_proc_entry(const char *name, struct proc_dir_entry *parent);

该函数中的参数name和parent与上述函数的参数意义相同。
在示例程序中,我们在卸载函数中完成上述几个文件的删除工作:

	remove_proc_entry("jiffies_too", example_dir);
	remove_proc_entry("foo", example_dir);
	remove_proc_entry("MODULE_NAME", NULL);
2.5读写proc文件

如果只是创建了虚拟文件,那么它并不能被读写。为此,我们必须为每个虚拟文件挂接读写函数,如果该虚拟文件是只读的,那么只需挂载相应的读函数。

正如上面所述,每个虚拟文件对应的proc_dir_entry结构都有read_proc和write_proc两个字段,它们均为函数指针,其各自的类型定义如下:

 typedef int (read_proc_t)(char *page, char **start, off_t off, int count, int *eof, void *data);
  typedef int (write_proc_t)(struct file *file, const char __user *buffer, unsigned long count, void *data);

如果要实现对虚拟文件的读写,则需要实现上述两个函数接口。对于我们的示例程序,我们的实现方法如下:

static int proc_read_foobar(char *page, char **start, off_t off, int count, int *eof, void *data)
{
	int len;
	struct fb_data_t *fb_data = (struct fb_data_t *)data;

	//将fb_data的数据写入page
	len = sprintf(page, "%s = %s\n", fb_data->name, fb_data->value);

	return len;
}

static int proc_write_foobar(struct file *file, const char *buffer, unsigned long count, void *data)
{
	int len;
	struct fb_data_t *fb_data = (struct fb_data_t *)data;

	if (count > FOOBAR_LEN)
		len = FOOBAR_LEN;
	else
		len = count;

	//写函数的核心语句,将用户态的buffer写入内核态的value中
	if (copy_from_user(fb_data->value, buffer, len))
		return -EFAULT;

	fb_data->value[len] = '\0';

	return len;
}

当用户读我们刚创建的虚拟文件时,该文件对应的read_proc函数将被调用。该函数将数据写入内核的缓冲区中。上述读函数的例子中,缓冲区即为page。当用户给虚拟文件写数据时,write_proc函数将被调用,该函数从缓冲区buffer中读取count个字节的数据。

3.测试

接下来我们将进行一系列的读写测试。由于我们只为jiffies与其符号链接文件jiffies_too实现了读回调函数,因此它们为只读文件,当对这两个文件进行写操作时就会出现错误;对于foo和bar文件,我们为其实现了读、写函数,因此既可以对它们进行读操作也可以进行写操作。

root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies
jiffies = 833619
root@edsionte-desktop:/proc/edsionte_procfs# cat jiffies_too
jiffies = 834442
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
bar = bar
root@edsionte-desktop:/proc/edsionte_procfs# cat foo
foo = foo
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies
bash: echo: 写操作出错: 输入/输出错误
root@edsionte-desktop:/proc/edsionte_procfs# echo "time" > jiffies_too
bash: echo: 写操作出错: 输入/输出错误
root@edsionte-desktop:/proc/edsionte_procfs# echo "hello" >> bar
root@edsionte-desktop:/proc/edsionte_procfs# cat bar
bar = hello

示例程序可以在此下载完成代码。

参考:

1.边干边学-Linux内核指导;作者: 李善平;出版社: 浙江大学出版社;

2.使用 /proc 文件系统来访问 Linux 内核的内容;
http://www.ibm.com/developerworks/cn/linux/l-proc.html

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