当前位置:Linux教程 - Linux - Linux内核编程(字符设备文件)

Linux内核编程(字符设备文件)

2.字符设备文件
那么,现在我们是原始级的内核程序员,我们知道如何写不做任何事情的内核模块。我们为自己而骄傲并且高昂起头来。但是不知何故我们感觉到缺了什么东西。患有精神紧张症的模块不是那么有意义。
内核模块同进程对话有两种主要途径。一种是通过设备文件(比如/dev 目录中的文件),另一种是使用proc文件系统。我们把一些东西写入内核的一个主要原因就是支持一些硬件设备,所以我们从设备文件开始。
设备文件的最初目的是允许进程同内核中的设备驱动通信,并且通过它们和物理设备通信(modem,终端,等等)。这种方法的实现如下:
每个设备驱动都对应着一定类型的硬件设备,并且被赋予一个主码。设备驱动的列表和它们的主码可以在in/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函数撤销设备的注册。
这些操作(注册和注销)是这两个函数的主要功能。内核中的函数不是象进程一样自发运行的,而是通过系统调用,或硬件中断或者内核中的其它部分(只要是调用具体的函数)被进程调用的。所以,当你向内和中增加代码时,你应该把它注册为具体某种事件的句柄,而当你把它删除的时候,你需要注销这个句柄。
设备驱动完全由四个设备_ 另一点我们需要记住的是,我们不能允许管理员随心所欲的删除内核模块。这是因为如果设备文件是被进程打开的,那么我们删除内核模块的时候,要使用这些文件就会导致访问正常的函数(读/写)所在的内存位置。如果幸运,那里不会有其他代码被装载,我们将得到一个恶性的错误信息。如果不行,另一个内核模块会被装载到同一个位置,这将意味着会跳入内核中另一个程序的中间,结果将是不可预料的恶劣。
通常你不希望一个函数做什么事情的时候,会从那个函数返回一个错误码(一个负数)。但这在cleanup_module中是不可能的,因为它是一个void型的函数。一旦cleanup_module被调用,这个模块就死掉了。然而有一个计数器记录着有多少个内核模块在使用这个模块,这个计数器称为索引计数器(/proc/modules中没行的最后一个数字)。如果这个数字不是0,删除就会失败。模块的索引计数器包含在变量mod_use_count_中。有定义好的处理这个变量的宏(MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT),所以我们一般使用宏而不是直接使用变量mod_use_count_,这样在以后实现变化的时候会带来安全性。

ex chardev.c

/* chardev.c
* Copyright (C) 1998-1999 by Ori Pomerantz
*
* Create a character device (read only)
*/

/* The necessary header files */

/* Standard in kernel modules */
#include /* Were doing kernel work */
#include /* Specifically, a module */

/* Deal with CONFIG_MODVERSIONS */
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include
#endif

/* For character devices */
#include /* The character device
* definitions are here */
#include /* A wrapper which does
* next to nothing at
* at present, but may
* help for compatibility
* with future versions
* of Linux */


/* In 2.2.3 /usr/include/linux/version.h includes
* a macro for this, but 2.0.35 doesnt - so I add
* it here if necessary. */
#ifndef KERNEL_VERSION
#define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c))
#endif


/* Conditional compilation. LINUX_VERSION_CODE is
* the code (as per KERNEL_VERSION) of this version. */
#if LINUX_VERSION_CODE > KERNEL_VERSION(2,2,0)
#include /* for put_user */
#endif



#define SUCCESS 0


/* Device Declarations **************************** */

/* The name for our device, as it will appear
* in /proc/devices */
#define DEVICE_NAME ""char_dev""


/* The maximum length of the message from the device */
#define BUF_LEN 80

/* Is the device open right now? Used to prevent
* concurent access into the same device */
static int Device_Open = 0;

/* The message the device will give when asked */
static char Message[BUF_LEN];

/* How far did the process reading the message
* get? Useful if the message is larger than the size
* of the buffer we get to fill in device_read. */
static char *Message_Ptr;


/* This function is called whenever a process
* attempts to open the device file */
static int device_open(struct inode *inode,
struct file *file)
{
static int counter = 0;

#ifdef DEBUG
printk (""device_open(%p,%p) "", inode, file);
#endif

/* This is how you get the minor device number in
* case you have more than one physical device using
* the driver. */
printk(""Device: %d.%d "",
inode->i_rdev >> 8, inode->i_rdev & 0xFF);

/* We dont want to talk to two processes at the
* same time */
if (Device_Open)
return -EBUSY;

/* If this was a process, we would have had to
* be more careful here.
*
*In the case of processes, the danger would be
*that one process might have check Device_Open
*and then be replaced by the schedualer by another
*process which runs this function. Then, when
*the first process was back on the CPU, it would assume
*the device is still not open.
* However, Linux guarantees that a process wont
* be replaced while it is running in kernel context.
*
* In the case of SMP, one CPU might increment
*Device_Open while another CPU is here, right after the check.
*However, in version 2.0 of the kernel this is not a problem
*because theres a lock to guarantee only one CPU will
*be kernel module at the same time.
*This is bad in terms of performance, so version 2.2 changed it.
*Unfortunately, I dont have access to an SMP box
*to check how it works with SMP.
*/

Device_Open++;

/* Initialize the message. */
sprintf(Message,
""If I told you once, I told you %d times - %s"",
counter++,
""Hello, world "");
/* The only reason were allowed to do this sprintf
* is because the maximum length of the message
* (assuming 32 bit integers - up to 10 digits
* with the minus sign) is less than BUF_LEN, which
* is 80. BE CAREFUL NOT TO OVERFLOW BUFFERS,
* ESPECIALLY IN THE KERNEL!!!
*/

Message_Ptr = Message;

/* Make sure that the module isnt removed while
* the file is open by incrementing the usage count
* (the number of opened references to the module, if
* its not zero rmmod will fail)
*/
MOD_INC_USE_COUNT;

return SUCCESS;
}


/* This function is called when a process closes the
* device file. It doesnt have a return value in
* version 2.0.x because it cant fail (you must ALWAYS
* be able to close a device). In version 2.2.x it is
* allowed to fail - but we wont let it.
*/
#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) "", inode, file);
#endif

/* Were now ready for our next caller */
Device_Open --;

/* Decrement the usage count, otherwise once you
* opened the file youll never get rid of the module.
*/
MOD_DEC_USE_COUNT;

#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
return 0;
#endif
}


/* This function is called whenever a process which
* have already opened the device file attempts to
* read from it. */


#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_read(struct file *file,
char *buffer, /* The buffer to fill with data */
size_t length, /* The length of the buffer */
loff_t *offset) /* Our offset in the file */
#else
static int device_read(struct inode *inode,
struct file *file,
char *buffer, /* The buffer to fill with
* the data */
int length) /* The length of the buffer
* (mustnt write beyond that!) */
#endif
{
/* Number of bytes actually written to the buffer */
int bytes_read = 0;

/* If were at the end of the message, return 0
* (which signifies end of file) */
if (*Message_Ptr == 0)
return 0;

/* Actually put the data into the buffer */
while (length && *Message_Ptr) {

/* Because the buffer is in the user data segment,
* not the kernel data segment, assignment wouldnt
* work. Instead, we have to use put_user which
* copies data from the kernel data segment to the
* user data segment. */
put_user(*(Message_Ptr++), buffer++);


length --;
bytes_read ++;
}

#ifdef DEBUG
printk (""Read %d bytes, %d left "",
bytes_read, length);
#endif

/* Read functions are supposed to return the number
* of bytes actually inserted into the buffer */
return bytes_read;
}




/* This function is called when somebody tries to write
* into our device file - unsupported in this example. */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
static ssize_t device_write(struct file *file,
const char *buffer, /* The buffer */
size_t length, /* The length of the buffer */
loff_t *offset) /* Our offset in the file */
#else
static int device_write(struct inode *inode,
struct file *file,
const char *buffer,
int length)
#endif
{
return -EINVAL;
}




/* Module Declarations ***************************** */

/* The major device number for the device. This is
* global (well, static, which in this context is global
* within this file) because it has to be accessible
* both for registration and for release. */
static int Major;

/* This structure will hold the functions to be
* called when a process does something to the device
* we created. Since a pointer to this structure is
* kept in the devices table, it cant be local to
* init_module. NULL is for unimplemented functions. */


struct file_operations Fops = {
NULL, /* seek */
device_read,
device_write,
NULL, /* readdir */
NULL, /* select */
NULL, /* ioctl */
NULL, /* mmap */
device_open,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0)
NULL, /* flush */
#endif
device_release /* a.k.a. close */
};


/* Initialize the module - Register the character device */
int init_module()
{
/* Register the character device (atleast try) */
Major = module_register_chrdev(0,
DEVICE_NAME,
&Fops);

/* Negative values signify an error */
if (Major < 0) {
printk (""%s device failed with %d "",
""Sorry, registering the character"",
Major);
return Major;
}

printk (""%s The major device number is %d. "",
""Registeration is a success."",
Major);
printk (""If you want to talk to the device driver, "");
printk (""youll have to create a device file. "");
printk (""We suggest you use: "");
printk (""mknod c %d "", Major);
printk (""You can try different minor numbers %s"",
""and see what happens. "");

return 0;
}


/* Cleanup - unregister the appropriate file from /proc */
void cleanup_module()
{
int ret;

/* Unregister the device */
ret = module_unregister_chrdev(Major, DEVICE_NAME);

/* If theres an error, report it */
if (ret < 0)
printk(""Error in unregister_chrdev: %d "", ret);
}
2.1多内核版本源文件
系统调用是内核出示给进程的主要接口,在不同版本中一般是相同的。可能会增加新的系统,但是旧的系统的行为是不变的。向后兼容是必要的——新的内核版本不能打破正常的进程规律。在大多数情况下,设备文件是不变的。然而,内核中的内部接口是可以在不同版本间改变的。
Linux内核的版本分为稳定版(n.<偶数>.m)和发展版(n.<奇数>.m)。发展版包含了所有新奇的思想,包括那些在下一版中被认为是错的,或者被重新实现的。所以,你不能相信在那些版本中这些接口是保持不变的(这就是为什么我在本书中不厌其烦的支持不同接口。这是很大量的工作但是马上就会过时)。但是在稳定版中我们就可以认为接口是相同的,即使在修正版中(数字m所指的)。
MPG版本包括了对内核2.0.x和2.2.x的支持。这两种内核仍有不同之处,所以编译时要取决于内核版本而决定。方法是使用宏LINUX_VERSION_CODE。在a.b.c版中,这个宏的值是216a+28b+c。如果希望得到具体内核版本号,我们可以使用宏KERNEL_VERSION。在2.0.35版中没有定义这个宏,在需要时我们可以自己定义。