首先说明主设备号与次设备号
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
在编写Linux内核驱动程序的时候,如果不动态生成设备号的话,需要自己手动分配设备号,有可能你分配的设备号会与已有设备号相同而产生冲突。因此推荐自动分配设备号。使用下面的函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
该函数需要传递给它指定的第一个次设备号baseminor(一般为0)和要分配的设备数count,以及设备名,调用该函数后自动分配得到的设备号保存在dev中。
当使用了alloc_chrdev_region()动态分配设备号之后,需要依次使用:
cdev_init(struct cdev * cdev,const struct file_operations * fops)
和
cdev_add(struct cdev * p,dev_t dev,unsigned count)
将字符设备注册到内核中。通过上面三个函数就可以动态生成设备号了。
在卸载的时候需要使用:unregister_chrdev_region(dev_t from,unsigned count) 来释放设备编号
动态创建设备号之后,将驱动加载到内核,通过 : cat /proc/devices 命令可以查看设备号
如果上层应用程序需要访问驱动程序,则需要为该驱动创建设备节点。
如果手动创建设备结点需要这样做:(这里假设通过 cat /proc/devices 发现字符设备 CDEV_ZHU的设备号为 254)
$mknod /dev/CDEV_ZHU c 254 0
如果我们在驱动里面动态创建的话需要这样做:
cdev_class = class_create(owner,name) // cdev_class 为 struct class 类型
然后使用:
device_create(_cls,_parent,_devt,_device,_fmt)
当动态创建了设备节点之后,在卸载的时候需要使用:
device_destroy(_cls,_device) 和 class_destroy(struct class * cls)
来销毁设备和类。
下面给出一组测试代码:(该组代码实现了应用程序通过打开驱动访问和修改驱动的一个全局变量 “global_var”)
/*驱动部分:globalvar.c */
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <asm/device.h> //下面这三个头文件是由于动态创建需要加的
#include <linux/device.h>
#include <linux/cdev.h>
MODULE_LICENSE("GPL");
#define DEVICE_NAME "CDEV_ZHU"
static struct class *cdev_class;
static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*);
static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*);
//初始化字符设备驱动的 file_operations 结构体
struct file_operations globalvar_fops =
{
read: globalvar_read,
write: globalvar_write,
};
static int global_var = 0; //CDEV_ZHU设备的全局变量
dev_t dev = 0; //这里是动态分配设备号和动态创建设备结点需要用到的
struct cdev dev_c;
static int __init globalvar_init(void)
{
int ret,err;
//注册设备驱动
ret = alloc_chrdev_region(&dev, 0, 1,DEVICE_NAME); //动态分配设备号
if (ret)
{
printk("globalvar register failure\n");
unregister_chrdev_region(dev,1);
return ret;
}
else
{
printk("globalvar register success\n");
}
cdev_init(&dev_c, &globalvar_fops);
err = cdev_add(&dev_c, dev, 1);
if(err)
{
printk(KERN_NOTICE "error %d adding FC_dev\n",err);
unregister_chrdev_region(dev, 1);
return err;
}
cdev_class = class_create(THIS_MODULE, DEVICE_NAME);//动态创建设备结点
if(IS_ERR(cdev_class))
{
printk("ERR:cannot create a cdev_class\n");
unregister_chrdev_region(dev, 1);
return -1;
}
device_create(cdev_class,NULL, dev, 0, DEVICE_NAME);
return ret;
}
static void __exit globalvar_exit(void)
{
//注销设备驱动
device_destroy(cdev_class, dev);
class_destroy(cdev_class);
unregister_chrdev_region(dev,1);
printk("globalvar_exit \n");
}
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
//将 global_var 从内核空间复制到用户空间
if(copy_to_user(buf, &global_var, sizeof(int)))
{
return - EFAULT;
}
return sizeof(int);
}
static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
//将用户空间的数据复制到内核空间的 global_var
if(copy_from_user(&global_var, buf, sizeof(int)))
{
return - EFAULT;
}
return sizeof(int);
}
module_init(globalvar_init);
module_exit(globalvar_exit);
/*应用程序: globalvartest.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd, num;
//打开"/dev/CDEV_ZHU"
fd = open("/dev/CDEV_ZHU", O_RDWR, S_IRUSR | S_IWUSR);
if (fd != -1 )
{
//初次读 global_var
read(fd, &num, sizeof(int));
printf("The globalvar is %d\n", num);
//写 global_var
printf("Please input the num written to globalvar\n");
scanf("%d", &num);
write(fd, &num, sizeof(int));
//再次读 global_var
read(fd, &num, sizeof(int));
printf("The globalvar is %d\n", num);
//关闭“/dev/CDEV_ZHU”
close(fd);
}
else
{
printf("Device open failure\n");
}
return 0;
}
说明:这个程序是我修改了“深入浅出Linux设备编程”这本书的代码的来的,在项目中使用动态创建设备节点和动态生成设备号比较方便,于是就在这里分享了。
使用一个简单的makefile将(驱动) globalvar.c 编译过后 使用 insmod globalvar.ko 将驱动加载到内核,然后就将globalvartest.c 生成的可执行文件运行起来就可以操作驱动中的全局变量了。不用像书上一样还要在命令行去创建设备节点。
我使用的内核版本是2.6.33.4 。