init_module()
函数,通常在该函 数中完成设备的注册。同样,模块在调用 rmmod 命令时被卸载,此时的入口点是 cleanup_module()
函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如 open()、read()、write()等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作,init_module()入口点函数则不需要完成其他如 read()、write() 之类功能。用户应用程序调用设备的一些功能是在设备驱动程序中定义的,也就是设备驱动程序的入口点,它是一个在内核中定义的 struct file_operations
结构,不会出现在用户空间的程序中,但它定义了常见文件 I/O 函数的入口,如下所示:
struct file_operations
{ loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *filp, char *buff, size_t count, loff_t *offp); ssize_t (*write) (struct file *filp, const char *buff, size_t count, loff_t *offp); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int cmd, unsigned long arg); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *); int (*fasync) (int, struct file *, int); int (*check_media_change) (kdev_t dev); int (*revalidate) (kdev_t dev); int (*lock) (struct file *, int, struct file_lock *);
};
系统调用函数通过内核,最终调用对应的 struct file_operations 结构的接口函数(例如,open()文件操作是通过调用对应文件的 file_operations 结构的 open 函数接口而被实现)。当然,每个设备的驱动程序不一定要实现其中所有的函 数操作,若不需要定义实现时,则只需将其设为 NULL 即可。
打开设备open()
释放设备release()
读写设备read()、write()
作用:把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,也就是将 内核空间缓冲区里的数据复制到用户空间的缓冲区中或者相反。
注意:虽然这个过程看起来很简单,但是内核空间地址和应用空间地址是有很大区别的,其中一个区别是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以不能使用诸如 memcpy() 之类的函数来完成这样的操作。在这里要使用 copy_to_user()或 copy_from_user()等专门实现用户空间和内核空间的数据交换的函数。这两个函数不仅实现了用户空间和内核空间的数据转换,而且还会检查用户空间指针的有效性。 如果指针无效,那么就不进行复制。
/* 位于 */
unsigned long copy_to_user(void *to, const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const void *from, unsigned long count); /*
参数:to:数据目的缓冲区from:数据源缓冲区count:数据长度
返回值:成功:写入的数据长度失败:-EFAULT
*/
控制设备ioctl()
struct inode 结构提供了关于设备文件/dev/driver(假设此设备名为 driver)的信息,struct file
结构提供关于被打开的文件信息,主要用于与文件系统对应的设备驱动程序使用。struct file 结构较为重要,这里列出了 它的定义:
struct file
{ mode_t f_mode; /*标识文件是否可读或可写,FMODE_READ 或 FMODE_WRITE*/ dev_t f_rdev; /* 用于/dev/tty */ off_t f_pos; /* 当前文件位移 */ unsigned short f_flags; /* 文件标志,如 O_RDONLY、O_NONBLOCK 和 O_SYNC */ unsigned short f_count; /* 打开的文件数目 */ unsigned short f_reada; struct inode *f_inode; /*指向 inode 的结构指针 */ struct file_operations *f_op; /* 文件操作函数索引指针 */
};
在Linux2.4以前,内核中所有已分配的字符设备编号都记录在一个名为 chrdevs ,元素个数为255的散列表里。该散列表中的每一个元素是一个 char_device_struct 结构,代表主设备号相同的一组设备。它在内核中的定义如下:
static struct char_device_struct {struct char_device_struct *next; // 指向散列表中的下一个指针unsigned int major; // 主设备号unsigned int baseminor; // 起始次设备号int minorct; // 设备编号数char name[64]; // 设备驱动名struct file_operations *fops; // 指向该设备对应的文件操作函数结构体指针struct cdev *cdev; // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
dev_t
类型来描述设备号(dev_t 是 32 位数值类型,其中高 12 位表示主设备号, 低 20 位表示次设备号)。用两个宏 MAJOR 和 MINOR 分别获得 dev_t 设备号的主设备号和次设备号,而 且用 MKDEV 宏来实现逆过程,即组合主设备号和次设备号而获得 dev_t 类型设备号。分配设备号有静态和动态的两种方法:
register_chrdev_region()
)是指在事先知道设备主设备号的情况下,通过参数函数指定第一个设备号(它的次设备号通常为 0)而向系统申请分配一定数目的设 备号。alloc_chrdev_region()
)是指通过参数仅设置第一个次设备号(通常为 0,事先不会知道主设备号)和要分配的设备数目而系统动态分配所需的设备号。设备号的释放:通过unregister_chrdev_region()
释放已分配的(无论是静态的还是动态的)设备号。
它们的函数格式如下所示:
/* 静态分配:*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{struct char_device_struct *cd;dev_t to = from + count;dev_t n, next;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);if (next > to)next = to;cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name);if (IS_ERR(cd))goto fail;}return 0;
fail:to = n;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));}return PTR_ERR(cd);
}/* 动态分配:*/
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);/*
from: 要分配的设备号的初始值,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0;
count:要分配(释放)的设备号数目,比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上.
name:要申请设备号的设备名称(在/proc/devices 和 sysfs 中显示)
dev:动态分配的第一个设备号成功:0(只限于两种注册函数)
出错:-1(只限于两种注册函数)
*/
struct cdev
结构来描述字符设备,我们在驱动程序中必须将已分配到的设备号以及设备操作接口( struct file_operations 结构)赋予 struct cdev 结构变量。在Linux2.4内核以前使用的是这种分配设备编号范围的函数:register_chrdev()
和unregister_chrdev()
。它每次都是粗粒度的分配一个主设备号和256个(0 ~ 255)次设备号(如果申请的主设备号为 0 则动态分配一个),如果执行成功,设备名就会出现在/proc/devices 文件里。
该函数内部自动分配了一个 cdev 结构,我们另外还需传入一个 file_operations 结构的指针,用来和新建的char_device_struct 结构体绑定,以后凡是相同主设备号(即所有256个共享该主设备号的次设备号设备)的设备均使用同一个file_operations,不管你实际使用了几个次设备号,默认都会将相应主设备号下的256个次设备号连续注册,造成了极大的浪费。
其定义位于头文件
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{struct char_device_struct *cd;struct cdev *cdev;char *s;int err = -ENOMEM;cd = __register_chrdev_region(major, 0, 256, name);if (IS_ERR(cd))return PTR_ERR(cd);cdev = cdev_alloc();if (!cdev)goto out2;cdev->owner = fops->owner;cdev->ops = fops;kobject_set_name(&cdev->kobj, "%s", name);for (s = strchr(kobject_name(&cdev->kobj),'/'); s; s = strchr(s, '/'))*s = '!';err = cdev_add(cdev, MKDEV(cd->major, 0), 256);if (err)goto out;cd->cdev = cdev;return major ? 0 : cd->major;
out:kobject_put(&cdev->kobj);
out2:kfree(__unregister_chrdev_region(cd->major, 0, 256));return err;
}int unregister_chrdev(unsigned int major, const char *name);/*
参数:major:设备驱动程序向系统申请的主设备号,如果为 0 则系统为此驱动程序动态地分 配一个主设备号。name:设备名fops:对各个调用的入口点
返回值:成功:0,如果是动态分配主设备号,此返回所分配的主设备号。且设备名就会出现在/proc/devices 文件里。失败:-1
函数原型:位于头文件
sturct cdev *cdev_alloc(void);
void cdev_init(struct cdev *cdev, struct file_operations *fops);
int cdev_add (struct cdev *cdev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);
/*
参数:cdev:需要初始化/注册/删除的 struct cdev 结构fops:该字符设备的 file_operations 结构num:系统给该设备分配的第一个设备号(动态或静态方式取得的)count:该设备对应的设备号数量
返回值:成功:cdev_alloc:返回分配到的 struct cdev 结构指针cdev_add:返回 0 出错:cdev_alloc:返回 NULL cdev_add:返回 -1
*/
字符设备注册流程:
cdev_alloc()
函数向系统申请分配 struct cdev 结构;cdev_init()
函数初始化已分配到的结构并与 file_operations 结构关联起来。cdev_add()
函数将设备号与 struct cdev 结构进行关联,并向内核正式报告新设备的注册,这样新设备可以被用起来了。cls=class_create(THIS_MODULE, "hello");
class_device_create(cls,0, MKDEV(major,0), 0, "hello_1");
使用示例:在同一主设备号下创建两个设备域,并分别与不同的文件操作函数绑定。
/**创建两个字符设备,他们公用同一个主设备号;*但次设备号0~1对应第一个字符设备,使用hello1_fops文件操作符;* 次设备号2~3对应第二个字符设备,使用hello2_fops文件操作符;* 次设备号4不对应字符设备,不使用文件操作符;*/#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include static int hello_fops1_open(struct inode *inode, struct file *file)
{printk("open_hello1\n");return 0;
}static int hello_fops2_open (struct inode *inode, struct file *file)
{printk("open_hello2\n");return 0;
}/* 操作结构体1 */
static struct file_operations hello1_fops={.owner=THIS_MODULE,.open =hello_fops1_open,
};/* 操作结构体2 */
static struct file_operations hello2_fops={.owner=THIS_MODULE,.open =hello_fops2_open,
};static int major; //主设备
static struct cdev hello1_cdev; //保存 hello1_fops操作结构体的字符设备
static struct cdev hello2_cdev; //保存 hello2_fops操作结构体的字符设备
static struct class *cls;static int chrdev_region_init(void)
{dev_t devid;
#if 0major = register_chrdev(0,"hello",&hello_fops); //以前采用这种形式
#elseif(major){devid = MKDEV(major,0);register_chrdev_region(devid, 2, "hello"); }else{alloc_chrdev_region(&devid, 0, 2,"hello"); //动态分配字符设备major=MAJOR(devid);} cdev_init(&hello1_cdev, &hello1_fops); //初始化cdev,绑定fops结构体cdev_add(&hello1_cdev, MKDEV(major,0), 2); //注册cdev,即绑定(major,0~1)devid = MKDEV(major,2);register_chrdev_region(devid, 2, "hello2"); cdev_init(&hello2_cdev, &hello2_fops);cdev_add(&hello2_cdev,devid, 2); //注册cdev,即绑定(major,2~3)
#endifcls=class_create(THIS_MODULE, "hello");/*创建字符设备节点*/class_device_create(cls,0, MKDEV(major,0), 0, "hello0"); //对应hello_fops1操作结构体class_device_create(cls,0, MKDEV(major,1), 0, "hello1"); //对应hello_fops1操作结构体class_device_create(cls,0, MKDEV(major,2), 0, "hello2"); //对应hello_fops2操作结构体class_device_create(cls,0, MKDEV(major,3), 0, "hello3"); //对应hello_fops2操作结构体class_device_create(cls,0, MKDEV(major,4), 0, "hello4"); //对应空return 0;
}void chrdev_region_exit(void)
{class_device_destroy(cls, MKDEV(major,4));class_device_destroy(cls, MKDEV(major,3));class_device_destroy(cls, MKDEV(major,2));class_device_destroy(cls, MKDEV(major,1));class_device_destroy(cls, MKDEV(major,0));class_destroy(cls);cdev_del(&hello1_cdev); unregister_chrdev_region(MKDEV(major,0), 2); //注销(major,0)~(major,1)cdev_del(&hello2_cdev); unregister_chrdev_region(MKDEV(major,2), 2); //注销(major,2)~(major,3)
} module_init(chrdev_region_init);
module_exit(chrdev_region_exit);
MODULE_LICENSE("GPL");
#include
#include
#include
#include
#include
#include void print_useg(char arg[]) //打印使用帮助信息
{printf("useg: \n");printf("%s [dev]\n",arg);
}int main(int argc,char **argv)
{int fd;if(argc!=2){print_useg(argv[0]);return -1;}fd=open(argv[1],O_RDWR);if(fd<0)printf("can't open %s \n",argv[1]);elseprintf("can open %s \n",argv[1]);return 0;
}
装载驱动后,进行测试,得到如下结果:
# ls /dev/hello* -l
crw-rw---- 1 0 0 252, 0 Jan 1 00:12 /dev/hello0
crw-rw---- 1 0 0 252, 1 Jan 1 00:12 /dev/hello1
crw-rw---- 1 0 0 252, 2 Jan 1 00:12 /dev/hello2
crw-rw---- 1 0 0 252, 3 Jan 1 00:12 /dev/hello3
crw-rw---- 1 0 0 252, 4 Jan 1 00:12 /dev/hello4#./a.out /dev/hello0 //打开/dev/hello0时,调用的是hello1_fops里的.open()
open hello0
#
#
#./a.out /dev/hello2 //打开/dev/hello2时,调用的是hello1_fops里的.open()
open hello2
#
#
#./a.out /dev/hello4 //打开无效,因为在驱动代码里没有分配次设备号4的操作结构体
can't open /dev/hello4
起始不管是静态还是动态分配,内核中设备号的分配最终都是调用 __register_chrdev_region()
函数实现的,首先来看一下__register_chrdev_region
函数的定义:
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)
{struct char_device_struct *cd, **cp;int ret = 0;int i;cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);if (cd == NULL)return ERR_PTR(-ENOMEM);mutex_lock(&chrdevs_lock);if (major == 0) {for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--)if (chrdevs[i] == NULL)break;if (i == 0) {ret = -EBUSY;goto out;}major = i;ret = major;}cd->major = major;cd->baseminor = baseminor;cd->minorct = minorct;strncpy(cd->name,name, 64);i = major_to_index(major);for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)if ((*cp)->major > major ||((*cp)->major == major && ( ((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)) ))break;/* Check for overlapping minor ranges. */if (*cp && (*cp)->major == major) {int old_min = (*cp)->baseminor;int old_max = (*cp)->baseminor + (*cp)->minorct - 1;int new_min = baseminor;int new_max = baseminor + minorct - 1;/* New driver overlaps from the left. */if (new_max >= old_min && new_max <= old_max) {ret = -EBUSY;goto out;}/* New driver overlaps from the right. */if (new_min <= old_max && new_min >= old_min) {ret = -EBUSY;goto out;}}cd->next = *cp;*cp = cd;mutex_unlock(&chrdevs_lock);return cd;
out:mutex_unlock(&chrdevs_lock);kfree(cd);return ERR_PTR(ret);
}
内存分配/释放
在应用程序中获取内存通常使用函数 malloc(),但在设备驱动程序中动态开辟内存可以以字节或页面为单 位。其中,以字节为单位分配内存的函数有 kmalloc(),注意的是,kmalloc()函数返回的是物理地址,而 malloc()等返回的是线性虚拟地址,因此在驱动程序中不能使用 malloc()函数。
与 malloc()不同,kmalloc() 申请空间有大小限制。长度是 2 的整次方,并且不会对所获取的内存空间清零。以页为单位分配内存的函数:
与之相对应的释放内存用也有 kfree()或 free_page 函数族。
基于字节的内存分配函数kmalloc()
void *kmalloc(unsigned int len,int flags)
基于字节的内存释放函数kfree()
void kfree(void *obj)
基于页的内存分配函数get_free_page()族函数
头文件:
原型:
unsigned long get_zeroed_page(int flags)
unsigned long __get_free_page(int flags)
unsigned long __get_free_page(int flags,unsigned long order)
unsigned long __get_dma_page(int flags,unsigned long order)
参数:
返回值:
基于页的内存释放函数 free_ page 族函数
头文件:
原型:
unsigned long free_page(unsigned long addr)
unsigned long free_pages(unsigned long addr, unsigned long order)
参数:
返回值:
信息打印
int printk(const char *fmt,...)
proc文件系统
/proc 文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统可以让用户可以和内核内部数据结构进行交互,获取有关系统和进程的有用信息,在运行时通过改变内核参数来改变设置。与其他文件系统不同,/proc 存在于内存之中而不是在硬盘上,可以通过“ls”查看/proc 文件系统的内容。
下图列出了/proc 文件系统的主要目录内容:
还有一些是以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一 个目录在/proc 下,以进程的 PID 号为目录名,它们是读取进程信息的接口。进程目录的结构如下
可以看到,/proc 文件系统体现了内核及进程运行的内容,在加载模块成功后,读者可以通过查看/proc/device 文件获得相关设备的主设备号。
上一篇:ACL与NAT