2012年(11)
分类: LINUX
2012-09-16 13:36:36
主设备号用于区分不同的驱动程序;
次设备号用于区分不同的设备(比如不同的分区);
设备号用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);
通常可以看到如下代码:
当打开某个设备节点(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 */
};
字符设备的注册指告诉内核系统中多了一个设备并且本设备(模块)支持该字符设备的操作:
udev的原理是设备注册时通过在/sys下创建类和设备(class_create、device_create等操作),udev会周期的扫描/sys从而在/dev下会自动的建立相应的设备节点。
• 设备的打开与关闭
操作设备文件的第一步是open系统调用,它最终会调用到设备驱动中的open函数,其中主要完成设备初始化的工作,按需要更新file的f_ops(file_operation的指针):
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由设备驱动提供。
内核基本不关心次设备号,驱动程序才关心次设备号,次设备号的使用方法:
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);
在驱动中使用阻塞型读写操作的例子:
poll(BSD)、select(system V)、epoll(linux)是一些系统调用,它们可以检查对一组文件描述符(struct file)的操作是否会挂起。之所以需要它们是因为,有时候应用程序需要操作多个文件,如果在一个文件上因为读写而挂起是不行的,所以应用程序在读写文件之前先检查一下文件的状态(可读或可写),然后再用non-blocking的方式去读写文件。
poll、select、epoll需要驱动程序的支持,驱动程序中有“poll”系统调用的接口:
1.对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应等待队列添加到poll_table,并不阻塞
2.返回表示是否能对设备进行无阻塞、写访问的掩码
非阻塞读写应用程序片段:
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