接下来就是实现open,close,read,write操作了,这个驱动什么都没干,所以很好理解,用户请求read系统调用时,这个虚拟设备反回相应长度的“A”字符串,用户write时,将内容显示到日志中。这里要注意的是,内核空间中不能使用用户态的malloc,而是使用kmalloc/kfree。而且,用户read/write提供的buf地址也是用户态的,内核自然不能直接访问,需要通过copy_to_user/copy_from_user 进行数据拷贝,具体如下:/* Open the device */ static int hello_open( struct inode *inode, struct file *filp ){ printk( KERN_NOTICE"Hello device open!
" ); return 0; } /* Close hello_device */ static int hello_release( struct inode *inode, struct file *filp ){ printk( KERN_NOTICE"Hello device close!
" ); return 0; } /* user read from hello device*/ ssize_t hello_read( struct file *flip, char __user *buf, size_t count,loff_t *f_pos){ ssize_t retval = 0; char *bank; bank = kmalloc(count+1, GFP_KERNEL ); if( bank == NULL ) return -1; memset( bank, "A",count ); if( copy_to_user( buf, bank, count ) ){ retval = -EFAULT; goto out; } retval += count; *(bank+count)=0; printk( KERN_NOTICE"hello: user read %d bytes from me. %s
",count,bank ); out: kfree(bank); return retval; }
/* write to hello device */ ssize_t hello_write( struct file *filp, const char __user *buf, size_t count, loff_t *f_pos ){ ssize_t retval = 0; char *bank = kmalloc( count ,GFP_KERNEL ); if( bank == NULL ) return retval; if( copy_from_user(bank, buf, count ) ){ retval = -EFAULT; printk( KERN_NOTICE"hello: write error
" ); goto out; } retval += count; printk( KERN_NOTICE"hello: user has written %d bytes to me: %s
",count, bank ); out: kfree(bank ); return retval; } 你可能会注意到open和release函数头中的file和inode结构体,inode是内核内部文件的表示,当其指向一个字符设备时,其中的i_cdev成员既包含了指向cdev结构的指针。而file表示打开的文件描述符,对一个文件,若打开多次,则会有多个file结构,但只有一个inode与之对应。 因为驱动工作在内核空间,不能使用用户空间的libc函数,所以程序中打印语句为内核提供的printk,而非printf,KERN_NOTICE宏其实标记的是日志级别(共有八个)不同级别的消息会记录到不同的地方。如果你运行本模块,可能会发现printk语句并没有输出到控制台,这是正常的,控制台只显示一定级别的消息。当日志级别小于console_loglevel时,消息才能显示出来。你可以通过dmsg命令看到这些信息,也可以通过修改日志级别使之输出到你的虚拟终端。 作好以上准备工作后,接下来就可以开始进行向内核申请主设备号了。设备号是干什么吃的?据LDD记载,对字符设备的访问是通过文件系统内的设备名称进行的。那些被称为特殊文件、设备文件的节点,通常位于/dev目录,如果ls -l 查看该目录,第一列中带有c标志的即为字符设备,有b标志的为块设备。而第5、6列所示的两个数字分别为设备的主、次设备号。通常,主设备号标识设备所用的驱动程序(现在大多设备仍然采用“一个主设备号对应一个驱动程序”的规则),次设备号用于确定设备,比如你有两块网卡,使用同一驱动,主设备号相同,那么他们将由次设备号区分。 /* Module housekeeping */ static int hello_init(void){ int result; dev_t dev = MKDEV( hello_major, 0 );/*to transfer major as dev_t type*/
/* alloc the major device number dynamicly */ result = alloc_chrdev_region(&dev, 0 ,1, "hello" ); if( result < 0 ){ printk( KERN_NOTICE"Hello: unable to get major %d
",hello_major ); return result; } hello_major = MAJOR(dev); /* set up devices, in this case, there is only one device */ printk( KERN_NOTICE"hello init. major:%d, minor:%d
",hello_major,0 ); //printk( KERN_ALERT"hello init: %d, %d
",hello_major,0 ); hello_setup_cdev(&hellow, 0 , &hello_ops );
return 0;
}
/* Exit routine */ static void hello_exit(void){ /* remove the cdev from kernel */ cdev_del(&hellow ); /* release the device numble alloced earlier */ unregister_chrdev_region( MKDEV( hello_major, 0 ), 1 ); printk( KERN_NOTICE"hello exit. major:%d,minor %d
",hello_major,0 ); } 这里主设备号的分配由alloc_chrdev_region(第一个参数为dev_t 指针,用来存放设备编号,第二个参数为要使用的第一个次设备号,通常为0,第三个参数为请求的连续设备编号个数)动态分配,当然也可以静态指定一个未被使用的主设备号,相应函数为register_chrdev_region,但不推荐这样做。在模块被卸载时(hello_exit),通过unregister_chrdev_region释放设备号。MKDEV宏将给出的主、次设备号转换成dev_t类型,MAJOR,MINOR分别从dev_t中析取主次设备号。这里几个函数的原型为:int register_chrdev_region(dev_t first, unsigned int count, char *name);int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);void unregister_chrdev_region(dev_t first, unsigned int count); 然后进入hello_setup_cdev函数,对设备进行初始化这里cdev结构体是内核内部使用来表示字符设备的。在内核调用设备操作之前,必须分配并注册一个或多个这样的结构。为了方便,没有动态使用cdev_alloc函数分配空间,而是定义了一个全局静态cdev变量。通常你可以将你的cdev嵌入到自定义的结构体中(这个驱动很naive,没有这么做),通过cdev_init 函数初始化。最后调用cdev_add(),注册cdev到内核。 /* set up the cdev stucture for a device */ static void hello_setup_cdev( struct cdev *dev, int minor, struct file_operations *fops ){ int err; int devno = MKDEV( hello_major, minor ); /* initialize the cdev struct */ cdev_init( dev,fops ); dev->owner = THIS_MODULE; err = cdev_add( dev, devno, 1 ); /* register the cdev in the kernel */ if( err ) printk( KERN_NOTICE"Error %d adding hello%d
",err ,minor ); }
最后module_init( hello_init ); module_exit( hello_exit );指定了模块初始化和关闭函数。MODULE_LICENSE( "Dual BSD/GPL" ); 指定模块使用的许可证能被内核识别的许可证有GPL、GPL v2、 Dual BSD/GPL、 Dual MPL/GPL、Proprietary(专有)等,如果模块没有显式标记许可证,则会被认定为“专有”,内核加载这样的模块会被“污染”。 /* register the init and exit routine of the module */ module_init( hello_init ); module_exit( hello_exit );MODULE_AUTHOR( "jabenwang" ); MODULE_LICENSE( "Dual BSD/GPL" ); 到这里,这个字符设备驱动已经完成,接下来就是编译它。