• 设备号
主设备号用于区分不同的驱动程序;
次设备号用于区分不同的设备(比如不同的分区);
设备号用dev_t来表示,它包括了主、次设备号,创建方法为:
MKDEV(int
major, int minor);
注册设备号的方法为:
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);
通常可以看到如下代码:
- if (scull_major) {
- dev = MKDEV(scull_major, scull_minor);
- result = register_chrdev_region(dev, scull_nr_devs, "scull");
- } else {
- result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
- scull_major = MAJOR(dev);
- }
- if (result < 0) {
- printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
- return result;
- }
让内核自动分配设备号的话我们无法事先预知设备号,从而无法预先在/dev下创建设备节点,但是在驱动注册了设备之后,可以读取/proc/devices下的文件以获得内核分配给该设备的设备号,然后再创建设备节点。创建设备节点的命令是mknod。
当打开某个设备节点(file结构体),内核会根据设备节点调用相应驱动程序。
• file结构体
打开的file的操作方法用结构体struct file_operations来描述,其中部分有:
loff_t (*llseek) (struct file *, loff_t,
int);
ssize_t (*read) (struct file *, char __user
*, size_t, loff_t *);
ssize_t (*write) (struct file *, const char
__user *, size_t, loff_t *);
unsigned int (*poll) (struct file *, struct
poll_table_struct *);
int (*ioctl) (struct inode *, struct file
*, unsigned int, unsigned long);
int (*mmap) (struct file *, struct
vm_area_struct *);
int (*open) (struct inode *, struct file
*);
每一个打开的文件(不管是普通文件还是设备文件),内核中都用一个struct file来描述,它总是在一个文件被打开(open系统调用)的时候创建,相当于一个文件描述符,struct file结构体中包括了:
loff_t f_pos;
unsigned int f_flags; // O_RDONLY, O_NONBLOCK等
struct file_operations *f_op;//文件在open的时候,将其指向某个struct file_operations
struct
dentry *f_dentry; //通过filp->f_dentry->d_inode访问inode
一个文件可以被多个进程打开,每个进程都有这个文件的文件描述符(struct file),但是系统中只有一个这样的文件,文件本身的信息用structure
inode来描述,其中只有少数几个与驱动编程有关:
struct inode{
...
dev_t i_rdev;
umode_t i_mode;
struct file_operations
*i_fop;
union {
struct
block_device *i_bdev;
struct
cdev *i_cdev;
};
....
};
• 设备的注册
内核中用cdev来表示字符设备,用户一般定义自己的数据结构,并包含cdev:
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
字符设备的注册指告诉内核系统中多了一个设备并且本设备(模块)支持该字符设备的操作:
- static void scull_setup_cdev(struct scull_dev *dev, int index)
- {
- int err, devno = MKDEV(scull_major, scull_minor + index);
- cdev_init(&dev->cdev, &scull_fops);
- dev->cdev.owner = THIS_MODULE;
- dev->cdev.ops = &scull_fops; //1.cdev的初始化
- err = cdev_add (&dev->cdev, devno, 1); //2.cdev的注册,添加到驱动模型中
- /* Fail gracefully if need be */
- if (err)
- printk(KERN_NOTICE "Error %d adding scull%d", err, index);
- }
scull_setup_cdev一般在模块初始化或者probe通过的情况下调用,因为其中注册了一种新的字符设备,之后内核就知道系统中有这么一个新的字符设备了。然后通过某种方法(查看/proc或者udev),系统为该设备在/dev下面创建设备节点(文件),每一个设备节点就有了inode。
udev的原理是设备注册时通过在/sys下创建类和设备(class_create、device_create等操作),udev会周期的扫描/sys从而在/dev下会自动的建立相应的设备节点。
• 设备的打开与关闭
操作设备文件的第一步是open系统调用,它最终会调用到设备驱动中的open函数,其中主要完成设备初始化的工作,按需要更新file的f_ops(file_operation的指针):
- int scull_open(struct inode *inode, struct file *filp)
- {
- struct scull_dev *dev; /* device information */
- dev = container_of(inode->i_cdev, struct scull_dev, cdev);
- filp->private_data = dev; /* for other methods */
- /* now trim to 0 the length of the device if open was write-only */
- if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
- scull_trim(dev); /* ignore errors */
- }
- return 0; /* success */
-
- }//有的时候会为open计数
在驱动开发中,文件读/写模式
filp->f_mode、标志filp->f_flags都是设备驱动关心的内容,而私有数据指针
filp->private_data在驱动中被广泛使用。大多被指向设备驱动自定义的用于描述设备的结构
体,filp->private_data在open中赋值。fork()不创建新文件,它只增加file结构体中的引用计数。close系统调用仅在文件的引用计数减为0时调用驱动的release方法。
系统调用open打开一个字符设备的时候,根据设备号(inode->i_rdev), 通过kobj_lookup()在cdev_map中查找对应的驱动程序,字符设备cdev默认的file_operations中定义了chrdev_open,在字符设备open操作的过程中,将inode所指向的file_operations替换成cdev所指向的file_operations。
应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是 file_operations。由于字符设备的上层没有文件系统,所以的file_operations由设备驱动提供。
内核基本不关心次设备号,驱动程序才关心次设备号,次设备号的使用方法:
- int minor_open (struct inode *inode, struct file *filp){
- switch (MINOR (inode->i_rdev)) {
- case 1:
- filp->f_op = &minor0_fops;
- break;
- case 2:
- filp->f_op = &minor1_fops;
- break;
- default:
- return -ENXIO;
- }
- if (filp->f_op && filp->f_op->open)
- return filp->f_op->open (inode, filp); //运行次设备号配的open()函数
- return 0;
- }
• 设备的读写操作
ssize_t read(struct file *filp, char __user
*buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char
__user *buff, size_t count, loff_t *offp);
其中的count是需要读写的字节数。返回值返回了实际读写的字节数,返回负数表示某种错误类型(如-EINTR、-EFAULT等)。实际读写的字节数可能会小于count,多数情况下,应用程序会重复进行读写操作。
因为读写涉及了内核与用户两个空间:
出于安全性等因素的考虑(用户空间的数据在交换区、用户空间指针不合法等等情况),两个空间的数据不能直接拷贝,而需要通过下述方式:
unsigned long copy_to_user(void __user *to,
const void *from, unsigned long count);
unsigned long copy_from_user(void *to, const
void __user *from, unsigned long count);
copy_from_user可能会导致调用的进程被挂起。
• 设备的IOCTL
ioctl用以处理非常规操作,比如光驱设备的弹出收拢、报错等等。ioctl系统调用的函数接口为:
int (*ioctl) (struct inode *inode, struct
file *filp,unsigned int cmd, unsigned long arg);
每个ioctl的cmd应该要做到系统唯一,cmd中包含了type、number、direction、size等信息,可以用过_IO、_IOR、_IOW、_IOWR等宏来生成(详见LDD第六章)。
驱动中ioctl的实现一般是一个switch结构,每个switch的分支都代表一个cmd,在进入switch之前应判断cmd的有效性,若为非法的cmd,应返回给libc(libc实现了系统调用)“-EINVAL”。
• 阻塞型读写(blocking I/O)
很多情况下,设备的读写并不直接读写硬件,而是驱动程序在硬件与上层软件之间建立了一个缓冲区,读写等系统调用都是在这个缓冲区上进行的。如果在进行读操作的时候,发现读缓冲区上没有数据可读,可以将当前进程挂起(sleep);如果在进行写操作的时候,发现写操作缓冲区上数据已满(新的数据写不下),也可以将当前进程挂起。当driver检测到设备有数据可读或者有空间可写时,再唤醒进程。
特别地,对于写操作,若应用程序要确保数据写入设备,则在驱动层需要实现fsync系统调用。
进程的挂起(sleep)在进程一篇中叙述过,被挂起的进程被移出活动进程组(不能被CPU调度,状态从TASK_RUNNING到TASK_INTERRUPTIBLE或UNINTERRUPTIBLE),并加入到某一个事件的等待队列。当事件发生后,等待队列上的进程才会被唤醒。
进程在拥有spinlock、seqlock、RCU lock等的情况是不能够sleep的(会造成其他进程的忙等,甚至死锁)。获取semaphore的进程虽然能够sleep,但是也要意识到,这也使得其它要获得该semaphore的进程sleep了。另外,在禁用中断的情况下,也是不应该sleep的。
进程的挂起与唤醒操作:
wait_event(queue, condition)
wait_event_interruptible(queue,
condition)
wait_event_timeout(queue, condition,
timeout)
wait_event_interruptible_timeout(queue,
condition, timeout)
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t
*queue);
在驱动中使用阻塞型读写操作的例子:
- static DECLARE_WAIT_QUEUE_HEAD(wq);
- static int flag = 0;
- ssize_t sleepy_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
- {
- printk(KERN_DEBUG "process %i (%s) going to sleep\n",urrent->pid, current->comm);
- wait_event_interruptible(wq, flag != 0);
- //要wq的数据类型为wait_queue_head_t,一般放在设备结构体中!
- flag = 0;
- printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
- return 0; /* EOF */
- }
- ssize_t sleepy_write (struct file *filp, const char __user *buf, size_t count,loff_t *pos)
- {
- printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",current->pid, current->comm);
- flag = 1;
- wake_up_interruptible(&wq);
- return count; /* succeed, to avoid retrial */
- }
之前说过,当读缓冲区没有数据,或者写缓冲区没有空间的时候,读写操作都会被挂起,但是一旦读缓冲区有数据来临,或者写缓冲区有空间,发起读写操作的进程就会被唤醒,唤醒的工作一般在ISR中进行)。另外,对file(打开文件的文件描述符)可以设置O_NONBLOCK标志,这样的话,读写操作就不应该被挂起,若是遇到读写缓冲区应该被挂起的状况,则应返回“-EAGAIN”。以下是一个支持non-blocking的读例子:- static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
- {
- struct scull_pipe *dev = filp->private_data;
- if (down_interruptible(&dev->sem))
- return -ERESTARTSYS;//返回给VFS,说明被signal打断了
- while (dev->rp == dev->wp) { /* nothing to read *///读缓冲区为空
- up(&dev->sem); /* release the lock */
- if (filp->f_flags & O_NONBLOCK) //设置了non-blocking
- return -EAGAIN;
- PDEBUG("\"%s\" reading: going to sleep\n", current->comm);
-
- if (wait_event_interruptible(dev->inq, (dev->rp != dev->wp)))
- return -ERESTARTSYS; /* signal: tell the fs layer to handle it */
-
- /* otherwise loop, but first reacquire the lock */
- if (down_interruptible(&dev->sem)) //说明被唤醒了!
- return -ERESTARTSYS;
- }
- /* ok, data is there, return something */
- if (dev->wp > dev->rp)
- count = min(count, (size_t)(dev->wp - dev->rp));
- else /* the write pointer has wrapped, return data up to dev->end */
- count = min(count, (size_t)(dev->end - dev->rp));
- if (copy_to_user(buf, dev->rp, count)) {
- up (&dev->sem);
- return -EFAULT;
- }
- dev->rp += count;
- if (dev->rp == dev->end)
- dev->rp = dev->buffer; /* wrapped */
-
- up (&dev->sem);
- /* finally, awake any writers and return */
- wake_up_interruptible(&dev->outq);
- PDEBUG("\"%s\" did read %li bytes\n",current->comm, (long)count);
- return count;
- }
• POLL和SELECT
poll(BSD)、select(system V)、epoll(linux)是一些系统调用,它们可以检查对一组文件描述符(struct file)的操作是否会挂起。之所以需要它们是因为,有时候应用程序需要操作多个文件,如果在一个文件上因为读写而挂起是不行的,所以应用程序在读写文件之前先检查一下文件的状态(可读或可写),然后再用non-blocking的方式去读写文件。
poll、select、epoll需要驱动程序的支持,驱动程序中有“poll”系统调用的接口:
- static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
- {
- struct scull_pipe *dev = filp->private_data;
- unsigned int mask = 0;
- /*
- * The buffer is circular; it is considered full
- * if "wp" is right behind "rp" and empty if the
- * two are equal.
- */
- down(&dev->sem);
- poll_wait(filp, &dev->inq, wait);
- poll_wait(filp, &dev->outq, wait);
- if (dev->rp != dev->wp) //判断读写指针
- mask |= POLLIN | POLLRDNORM; /* readable */
- if (spacefree(dev))
- mask |= POLLOUT | POLLWRNORM; /* writable */
- up(&dev->sem);
- return mask;
- }
poll()做的事情有:
1.对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应等待队列添加到poll_table,并不阻塞
2.返回表示是否能对设备进行无阻塞、写访问的掩码
非阻塞读写应用程序片段:
- fd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR);
- if (fd != - 1){
- while (1){
- FD_ZERO(&rfds);//将指定的文件描述符集清空
- FD_SET(fd, &rfds);//将fd加入文件描述符集合
- tv.tv_sec = 5;
- tv.tv_usec = 0;
- select(fd + 1, &rfds, NULL, NULL, &tv);//返回值记录了发生状态变化的fd的集合
-
- if (FD_ISSET(fd, &rfds))//判断某个文件描述符是否被置位{
- read(fd, &num, sizeof(int));
- printf("The globalvar is %d\n", num);
- if (num == 0){
- close(fd);
- break;
- }
- }
- else
- printf("No data within 5 seconds.\n");
- }
LDD的第六章还介绍了fcntl系统调用,这样应用程序可以通过signal异步获得驱动的通知,而不用通过poll等方式。
LDD的第六章还介绍了驱动设备的访问控制,比如打开状态、用户权限等。
• 参考:
Linux Device Drivers
Linux Kernel Development
http://blog.csdn.net/hfyinsdu/article/details/6563497
http://blog.csdn.net/engerled/article/details/6254289