学院首页>操作系统>Linux>Linux内核模块编程之字符设备文件

Linux内核模块编程之字符设备文件

作者:cherami 来源:yesky 添加时间:2006-5-21 15:40:19
字符设备文件 



  因此现在我们是大胆的内核程序员而且我们知道如何写什么也不做的内核模块。我们为自己而自豪并且可以高高的仰起我们的头。但是不知何故我们感到遗失了什么。令人紧张的模块并不是很有趣的。 



  内核模块有两种主要的途径和进程对话。一种是通过设备文件 (像 /dev 目录中的文件), 另一种是使用 proc 文件系统。因为写内核中的某些东西的一个主要的原因是去支持某种硬件,所以我们从设备文件开始。 



  设备文件的原始目的是允许进程和内核中的设备驱动程序通信以通过它们和物理设备通信 (调制解调器, 终端, 等等)。 这个办法的实现是像下面这样的。 



  每个设备驱动程序负责某种硬件,它被分配一个主设备号。驱动程序的列表和它们的主设备号可以在 /proc/devices找到。每一个物理设备由设备驱动程序控制且被分配一个次设备号。 /dev 目录被假设包含每一个这样的设备的被称之为设备文件的特殊文件,无论它是否被真正的安装在系统上。 



  例如,如果你 ls -l /dev/hd[ab]*,你将看到所有的可能被连接到系统上的IDE硬盘的分区。注意它们都使用相同的主设备号,3。但是次设备号彼此都不相同。 

否认申明: 这是假设你正字使用 PC 架构的系统。我不知道基于其他架构的Linux设备的情况。. 



  当系统被安装,所有的那些设备文件被 mknod 命令创建。从技术上说没有必须将它们放在 /dev目录的原因,这只是一个有用的惯例。像练习所示的那样,当为了测试的目的而创建一个设备文件,将它放在你编译内核模块的那个目录也许更有意义。 



  设备分为两种:字符设备和块设备。不同之处在于块设备对于请求有缓冲区,因此它们可以选择以什么顺序进行响应。对于存储设备而言这一点是很重要的,因为在读写连续的扇区时比远远的分离的扇区更快。另一个不同就是块设备只能以块为单位接受输入和返回输出(块的大小根据设备的不同而不同),而字符设备只能使用它们可能使用的或多或少的字节大小。大多数设备是字符设备,因为它们不需要这种缓冲而且不以固定块大小进行操作。你可以用ls -l区分一个设备文件是块设备还是字符设备.如果开头是“b”,那么它就是块设备;如果是“c”,那么就是字符设备。 



  这个模块分为两部分:登记设备模块部分和设备驱动部分。 init_module 调用 module_register_chrdev 而将设备驱动程序加入内核的字符设备驱动程序表。它也返回该设备将使用的主设备号。 cleanup_module 注销该设备。 



  这(登记什么和注销它)是那两种功能的常规功能。内核中的东西不想普通进程那样主动运行自己,而是由进程通过系统调用,或者由硬件通过中断,或者由内核的其他部分(简单的讲,由特殊的函数调用)进行调用。结果,当你向内核中加入代码,你被假设将之登记为某种事件句柄,而当你移除它时,你被假设出注销它。 



  严格意义上讲,设备驱动程序由四个device_函数组成,当某人试图用我们的主设备号的设备文件做什么事时它被调用。内核是通过 file_operations结构知道要调用它们的,Fops, 在设备被登记时被给出,它包含那四个函数的指针。 



  另一点我们在这必须记住的是我们不能允许内核模块在任何根感觉需要的时候被rmmod。原因是如果设备文件正被一个进程打开然后我们移除那个内核模块,这将使用那个文件,而这又将导致对那个适当的读写函数所在的内存区域的调用。如果幸运的话,没有其他的代码被加载到那儿,我们得到一个难看的错误消息。如果不幸运的话,另一个内核模块被加载到同一区域,这就意味着跳到内核中的另一个函数的中间,结果是不可预见的,但肯定不是什么好事。 



  通常,当你不允许什么事情发生,你会从被假设做这件事的函数返回一个用负数表示的错误代码。使用cleanup_module 是不可能的,因为它不返回任何值。一旦cleanup_module 被调用,模块即死亡了。然而,这儿有一个被称为引用计数器(在/proc/modules中的相应行的最后一个数字)的计数器计算有多少其他的内核模块正在使用该模块。如果该数字非零 rmmod 调用将失败。模块的引用计数器在变量mod_use_count_中. 因为有处理这个变量的宏定义 (MOD_INC_USE_COUNT and MOD_DEC_USE_COUNT),我们最好使用它们而不直接使用 mod_use_count_ ,这样,如果将来实现方法改变了我们会更安全。 

范例 chardev.c 



/* chardev.c 

* Copyright (C) 1998-1999 by Ori Pomerantz



* 创建一个字符设备(只读)

*/



/* 必要的头文件 */



/* 内核模块标准头文件 */

#include /* 内核工作 */

#include /* 明确指定是模块 */



/* 处理 CONFIG_MODVERSIONS */

#if CONFIG_MODVERSIONS==1

#define MODVERSIONS

#include 

#endif 



/* 对于字符设备 */

#include /* 字符设备定义 */

#include /* 目前对下一步没有任何用的包装

* 但对未来的Linux 版本在兼容性上可能有帮助*/



/* 在2.2.3版内核 /usr/include/linux/version.h 包括这个宏,但是

* 在2.0.35中没有 - 因此我在这加入它以便需要 */

#ifndef KERNEL_VERSION

#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))

#endif



/* 条件编译。 LINUX_VERSION_CODE 是版本代号(经 KERNEL_VERSION) 。 */

#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)

#include /* for put_user */

#endif



#define SUCCESS 0



/* 设备声明 **************************** */



/* 设备名,将出现在 /proc/devices */

#define DEVICE_NAME "char_dev"





/* 设备消息的最大长度 */

#define BUF_LEN 80



/* 设备正被打开吗? 用于防止对同一设备的同时访问 */

static int Device_Open = 0;



/* 当设备被要求时给出的信息 */

static char Message[BUF_LEN];



/* 进程读取消息到多远?如果消息的长度大于我们要填充进 device_read

* 的缓冲区的大小,这将有用。 */

static char *Message_Ptr;



/* 这个函数在一个进程试图打开设备文件时被调用 */

static int device_open(struct inode *inode, 

struct file *file)

{

static int counter = 0;



#ifdef DEBUG

printk ("device_open(%p,%p)\n", inode, file);

#endif



/* 这是当你有多个物理设备使用那个驱动程序时如何得到次设备号。 */

printk("Device: %d.%d\n", 

inode->i_rdev >> 8, inode->i_rdev & 0xFF);



/* 我们不想同时和两个进程通话 */

if (Device_Open)

return -EBUSY;



/* 如果这是一个进程,我们将必须在这儿更小心。

*

* 在进程的情况下,危险在于一个进程已经检查过 Device_Open ,

* 然后由于另一个进程运行这个函数而被调度程序代替。

* 当第一个进程处于后台,它会假设设备仍然没有打开。

*

* 然而,Linux 保证当一个进程在内核环境下运行时不能被代替。

*

* 在对称多处理的情况下,一个 CPU 可能增加Device_Open 而另一个CPU 也在这里

* 刚好在检查完后。然而在 2.0 版内核中这不成为问题,因为有一个锁保证在同一

* 时间只有一个CPU是内核模块。这在性能上不好,因此 2.2 版改变了它。

* 不幸的是,我不能使用对称多处理单元去检查它在对称多处理下是如何工作的。

*/



Device_Open++;



/* 初始化消息。 */

sprintf(Message, 

"If I told you once, I told you %d times - %s",

counter++,

"Hello, world\n");

/* 我们允许用 sprintf 的唯一原因是消息(假设是32位整数,等于10位带符号的十进制数)

* 的最大长度小于 BUF_LEN的大小--80。在内核中尤其要注意缓冲区溢出!!!

*/ 



Message_Ptr = Message;



/* 通过增加使用计数确保模块在文件正被打开时不被移除

* (对该模块的打开的引用数非零时rmmod 将失败)

*/

MOD_INC_USE_COUNT;



return SUCCESS;

}



/* 这个函数在一个进程关闭设备文件时被调用。在 2.0.x 版中没有返回值,因为它不能失败

* (你必须总可以关闭设备)。在 2.2.x 版中允许失败 - 但我们不能允许。

*/

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

static int device_release(struct inode *inode, 

struct file *file)

#else 

static void device_release(struct inode *inode, 

struct file *file)

#endif

{

#ifdef DEBUG

printk ("device_release(%p,%p)\n", inode, file);

#endif



/* 现在为下一个调用者做准备 */

Device_Open --;



/* 减小使用计数,否则一旦你打开了文件你将永远不能移除该模块。*/

MOD_DEC_USE_COUNT;



#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

return 0;

#endif

}



/* 这个函数在一个已经打开设备文件而试图从它读的时候被调用。 */



#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

static ssize_t device_read(struct file *file,

char *buffer, /* 填充数据的缓冲区 */

size_t length, /* 缓冲区长度 */

loff_t *offset) /* 文件偏移量 */

#else

static int device_read(struct inode *inode,

struct file *file,

char *buffer, /* 缓冲区*/ 

int length) /* 缓冲区长度 (写的长度一定不能超过它!) */

#endif

{

/* 实际上写入缓冲区的字节数 */

int bytes_read = 0;



/* 如果在消息尾,返回 0 (表示文件尾) */

if (*Message_Ptr == 0)

return 0;



/* 实际上将数据放入缓冲区 */

while (length && *Message_Ptr) {



/* 因为缓冲区在用户数据段而不在内核数据段,靠分配是不行的。

* 我们不得不使用 put_user 将数据从内核数据段拷贝到用户数据段*/

put_user(*(Message_Ptr++), buffer++);





length --;

bytes_read ++;

}



#ifdef DEBUG

printk ("Read %d bytes, %d left\n",

bytes_read, length);

#endif



/* 读函数被假设返回实际上插入缓冲区的字节数 */

return bytes_read;

}



/* 这个函数在某人试图向我们的设备中写时被调用--在这个例子中不被支持 */

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

static ssize_t device_write(struct file *file,

const char *buffer, /* 缓冲区 */

size_t length, /* 缓冲区长度 */

loff_t *offset) /* 文件偏移量 */

#else

static int device_write(struct inode *inode,

struct file *file,

const char *buffer,

int length)

#endif

{

return -EINVAL;

}



/* 模块声明 ***************************** */



/* 设备的主设备号。这是全局的(是的,是静态的,在这个文件中是全局的)

* 因为在登记和释放时它都应可见。 */

static int Major;



/* 当一个进程对我们创建的设备做什么时,这个结构将保存被调用的函数。

* 既然这个结构的指针保存在设备表中,所以它不能对init_module是局部的.

* NULL 代替未实现的函数。 */



struct file_operations Fops = {

NULL, /* 寻找 */

device_read, 

device_write,

NULL, /* 读目录 */

NULL, /* 选择 */

NULL, /* ioctl */

NULL, /* mmap */

device_open,

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)

NULL, /* 刷新 */

#endif

device_release /* 关闭 */

};



/* 初始化模块 - 登记字符设备 */

int init_module()

{

/* 登记字符设备(至少试图登记) */

Major = module_register_chrdev(0, 

DEVICE_NAME,

&Fops);



/* 负数表示错误 */

if (Major < 0) {

printk ("%s device failed with %d\n",

"Sorry, registering the character",

Major);

return Major;

}



printk ("%s The major device number is %d.\n",

"Registeration is a success.",

Major);

printk ("If you want to talk to the device driver,\n");

printk ("you'll have to create a device file. \n");

printk ("We suggest you use:\n");

printk ("mknod c %d \n", Major);

printk ("You can try different minor numbers %s",

"and see what happens.\n");



return 0;

}





/* 清除 - 从 from /proc 注销适当的文件*/

void cleanup_module()

{

int ret;



/* 注销设备 */

ret = module_unregister_chrdev(Major, DEVICE_NAME);



/* 如果有错则报告 */ 

if (ret < 0)

printk("Error in unregister_chrdev: %d\n", ret);

}

多内核版本源文件 



  内核展现给进程的主要界面是系统调用,它通常跨版本保持相同。新的系统调用被加入,但通常老的保持和原来严格的一样。这对于向后兼容性是必要的--新的内核版本不应打破常规的进程。在大多情况下,设备文件也将保持相同。另一方面,和内核内部的接口可以并且在版本之间有改变。 



  Linux内核版本分为稳定版 (n.<偶数>.m) 和开发版 (n.<奇数>.m).开发版包含所有的好的新思想,包括那些被认为是错误或需要在下一版重新实现的东西。结果,你不能期盼在那些版本中界面保持相同(这也是我为什么不在这本书中操心去支持它的原因,那需要太多的工作并且很快就过时了)。另一方面,在稳定版中我们可以期盼界面保持相同,除了错误修订版(数字m)。 



  这个版本的内核模块编程指南包括对 2.0.x 和2.2.x 内核版本的支持。既然这两个版本间有差异,这就需要根据版本进行条件编译。可以使用宏 LINUX_VERSION_CODE来做这件事。在 a.b.c 版的内核中,这个宏的值是 216a+28b+c。为了 得到某个内核版本的值,我们可以使用 KERNEL_VERSION 宏. 因为在 2.0.35版中没有定义它, 我们可以在必要的时候自己定义它。 
站内搜索