|
|
|
|
# 32 | 字符设备(上):如何建立直销模式?
|
|
|
|
|
|
|
|
|
|
上一节,我们讲了输入输出设备的层次模型,还是比较复杂的,块设备尤其复杂。这一节为了让你更清晰地了解设备驱动程序的架构,我们先来讲稍微简单一点的字符设备驱动。
|
|
|
|
|
|
|
|
|
|
这一节,我找了两个比较简单的字符设备驱动来解析一下。一个是输入字符设备,鼠标。代码在drivers/input/mouse/logibm.c这里。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/*
|
|
|
|
|
* Logitech Bus Mouse Driver for Linux
|
|
|
|
|
*/
|
|
|
|
|
module_init(logibm_init);
|
|
|
|
|
module_exit(logibm_exit);
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
另外一个是输出字符设备,打印机,代码drivers/char/lp.c这里。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/*
|
|
|
|
|
* Generic parallel printer driver
|
|
|
|
|
*/
|
|
|
|
|
module_init(lp_init_module);
|
|
|
|
|
module_exit(lp_cleanup_module);
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 内核模块
|
|
|
|
|
|
|
|
|
|
上一节,我们讲过,设备驱动程序是一个内核模块,以ko的文件形式存在,可以通过insmod加载到内核中。那我们首先来看一下,怎么样才能构建一个内核模块呢?
|
|
|
|
|
|
|
|
|
|
一个内核模块应该由以下几部分组成。
|
|
|
|
|
|
|
|
|
|
**第一部分,头文件部分**。一般的内核模块,都需要include下面两个头文件:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
#include <linux/module.h>
|
|
|
|
|
#include <linux/init.h>
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
如果你去看上面两个驱动程序,都能找到这两个头文件。当然如果需要的话,我们还可以引入更多的头文件。
|
|
|
|
|
|
|
|
|
|
**第二部分,定义一些函数,用于处理内核模块的主要逻辑**。例如打开、关闭、读取、写入设备的函数或者响应中断的函数。
|
|
|
|
|
|
|
|
|
|
例如,logibm.c里面就定义了logibm\_open。logibm\_close就是处理打开和关闭的,定义了logibm\_interrupt就是用来响应中断的。再如,lp.c里面就定义了lp\_read,lp\_write就是处理读写的。
|
|
|
|
|
|
|
|
|
|
**第三部分,定义一个file\_operations结构**。前面我们讲过,设备是可以通过文件系统的接口进行访问的。咱们讲文件系统的时候说过,对于某种文件系统的操作,都是放在file\_operations里面的。例如ext4就定义了这么一个结构,里面都是ext4\_xxx之类的函数。设备要想被文件系统的接口操作,也需要定义这样一个结构。
|
|
|
|
|
|
|
|
|
|
例如,lp.c里面就定义了这样一个结构。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static const struct file_operations lp_fops = {
|
|
|
|
|
.owner = THIS_MODULE,
|
|
|
|
|
.write = lp_write,
|
|
|
|
|
.unlocked_ioctl = lp_ioctl,
|
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
|
|
|
.compat_ioctl = lp_compat_ioctl,
|
|
|
|
|
#endif
|
|
|
|
|
.open = lp_open,
|
|
|
|
|
.release = lp_release,
|
|
|
|
|
#ifdef CONFIG_PARPORT_1284
|
|
|
|
|
.read = lp_read,
|
|
|
|
|
#endif
|
|
|
|
|
.llseek = noop_llseek,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在logibm.c里面,我们找不到这样的结构,是因为它属于众多输入设备的一种,而输入设备的操作被统一定义在drivers/input/input.c里面,logibm.c只是定义了一些自己独有的操作。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static const struct file_operations input_devices_fileops = {
|
|
|
|
|
.owner = THIS_MODULE,
|
|
|
|
|
.open = input_proc_devices_open,
|
|
|
|
|
.poll = input_proc_devices_poll,
|
|
|
|
|
.read = seq_read,
|
|
|
|
|
.llseek = seq_lseek,
|
|
|
|
|
.release = seq_release,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**第四部分,定义整个模块的初始化函数和退出函数**,用于加载和卸载这个ko的时候调用。
|
|
|
|
|
|
|
|
|
|
例如lp.c就定义了lp\_init\_module和lp\_cleanup\_module,logibm.c就定义了logibm\_init和logibm\_exit。
|
|
|
|
|
|
|
|
|
|
**第五部分,调用module\_init和module\_exit**,分别指向上面两个初始化函数和退出函数。就像本节最开头展示的一样。
|
|
|
|
|
|
|
|
|
|
**第六部分,声明一下lisense,调用MODULE\_LICENSE**。
|
|
|
|
|
|
|
|
|
|
有了这六部分,一个内核模块就基本合格了,可以工作了。
|
|
|
|
|
|
|
|
|
|
## 打开字符设备
|
|
|
|
|
|
|
|
|
|
字符设备可不是一个普通的内核模块,它有自己独特的行为。接下来,我们就沿着打开一个字符设备的过程,看看字符设备这个内核模块做了哪些特殊的事情。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/2e/e6/2e29767e84b299324ea7fc524a3dcee6.jpeg)
|
|
|
|
|
|
|
|
|
|
要使用一个字符设备,我们首先要把写好的内核模块,通过insmod加载进内核。这个时候,先调用的就是module\_init调用的初始化函数。
|
|
|
|
|
|
|
|
|
|
例如,在lp.c的初始化函数lp\_init对应的代码如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static int __init lp_init (void)
|
|
|
|
|
{
|
|
|
|
|
......
|
|
|
|
|
if (register_chrdev (LP_MAJOR, "lp", &lp_fops)) {
|
|
|
|
|
printk (KERN_ERR "lp: unable to get major %d\n", LP_MAJOR);
|
|
|
|
|
return -EIO;
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int __register_chrdev(unsigned int major, unsigned int baseminor,
|
|
|
|
|
unsigned int count, const char *name,
|
|
|
|
|
const struct file_operations *fops)
|
|
|
|
|
{
|
|
|
|
|
struct char_device_struct *cd;
|
|
|
|
|
struct cdev *cdev;
|
|
|
|
|
int err = -ENOMEM;
|
|
|
|
|
......
|
|
|
|
|
cd = __register_chrdev_region(major, baseminor, count, name);
|
|
|
|
|
cdev = cdev_alloc();
|
|
|
|
|
cdev->owner = fops->owner;
|
|
|
|
|
cdev->ops = fops;
|
|
|
|
|
kobject_set_name(&cdev->kobj, "%s", name);
|
|
|
|
|
err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
|
|
|
|
|
cd->cdev = cdev;
|
|
|
|
|
return major ? 0 : cd->major;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在字符设备驱动的内核模块加载的时候,最重要的一件事情就是,注册这个字符设备。注册的方式是调用\_\_register\_chrdev\_region,注册字符设备的主次设备号和名称,然后分配一个struct cdev结构,将cdev的ops成员变量指向这个模块声明的file\_operations。然后,cdev\_add会将这个字符设备添加到内核中一个叫作struct kobj\_map \*cdev\_map的结构,来统一管理所有字符设备。
|
|
|
|
|
|
|
|
|
|
其中,MKDEV(cd->major, baseminor)表示将主设备号和次设备号生成一个dev\_t的整数,然后将这个整数dev\_t和cdev关联起来。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/**
|
|
|
|
|
* cdev_add() - add a char device to the system
|
|
|
|
|
* @p: the cdev structure for the device
|
|
|
|
|
* @dev: the first device number for which this device is responsible
|
|
|
|
|
* @count: the number of consecutive minor numbers corresponding to this
|
|
|
|
|
* device
|
|
|
|
|
*
|
|
|
|
|
* cdev_add() adds the device represented by @p to the system, making it
|
|
|
|
|
* live immediately. A negative error code is returned on failure.
|
|
|
|
|
*/
|
|
|
|
|
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
|
|
|
|
|
{
|
|
|
|
|
int error;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p->dev = dev;
|
|
|
|
|
p->count = count;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error = kobj_map(cdev_map, dev, count, NULL,
|
|
|
|
|
exact_match, exact_lock, p);
|
|
|
|
|
kobject_get(p->kobj.parent);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在logibm.c中,我们在logibm\_init找不到注册字符设备,这是因为input.c里面的初始化函数input\_init会调用register\_chrdev\_region,注册输入的字符设备,会在logibm\_init中调用input\_register\_device,将logibm.c这个字符设备注册到input.c里面去,这就相当于input.c对多个输入字符设备进行统一的管理。
|
|
|
|
|
|
|
|
|
|
内核模块加载完毕后,接下来要通过mknod在/dev下面创建一个设备文件,只有有了这个设备文件,我们才能通过文件系统的接口,对这个设备文件进行操作。
|
|
|
|
|
|
|
|
|
|
mknod也是一个系统调用,定义如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
SYSCALL_DEFINE3(mknod, const char __user *, filename, umode_t, mode, unsigned, dev)
|
|
|
|
|
{
|
|
|
|
|
return sys_mknodat(AT_FDCWD, filename, mode, dev);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode,
|
|
|
|
|
unsigned, dev)
|
|
|
|
|
{
|
|
|
|
|
struct dentry *dentry;
|
|
|
|
|
struct path path;
|
|
|
|
|
......
|
|
|
|
|
dentry = user_path_create(dfd, filename, &path, lookup_flags);
|
|
|
|
|
......
|
|
|
|
|
switch (mode & S_IFMT) {
|
|
|
|
|
......
|
|
|
|
|
case S_IFCHR: case S_IFBLK:
|
|
|
|
|
error = vfs_mknod(path.dentry->d_inode,dentry,mode,
|
|
|
|
|
new_decode_dev(dev));
|
|
|
|
|
break;
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们可以在这个系统调用里看到,在文件系统上,顺着路径找到/dev/xxx所在的文件夹,然后为这个新创建的设备文件创建一个dentry。这是维护文件和inode之间的关联关系的结构。
|
|
|
|
|
|
|
|
|
|
接下来,如果是字符文件S\_IFCHR或者设备文件S\_IFBLK,我们就调用vfs\_mknod。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
|
|
|
|
|
{
|
|
|
|
|
......
|
|
|
|
|
error = dir->i_op->mknod(dir, dentry, mode, dev);
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里需要调用对应的文件系统的inode\_operations。应该调用哪个文件系统呢?
|
|
|
|
|
|
|
|
|
|
如果我们在linux下面执行mount命令,能看到下面这一行:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
devtmpfs on /dev type devtmpfs (rw,nosuid,size=3989584k,nr_inodes=997396,mode=755)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
也就是说,/dev下面的文件系统的名称为devtmpfs,我们可以在内核中找到它。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static struct dentry *dev_mount(struct file_system_type *fs_type, int flags,
|
|
|
|
|
const char *dev_name, void *data)
|
|
|
|
|
{
|
|
|
|
|
#ifdef CONFIG_TMPFS
|
|
|
|
|
return mount_single(fs_type, flags, data, shmem_fill_super);
|
|
|
|
|
#else
|
|
|
|
|
return mount_single(fs_type, flags, data, ramfs_fill_super);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static struct file_system_type dev_fs_type = {
|
|
|
|
|
.name = "devtmpfs",
|
|
|
|
|
.mount = dev_mount,
|
|
|
|
|
.kill_sb = kill_litter_super,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
从这里可以看出,devtmpfs在挂载的时候,有两种模式,一种是ramfs,一种是shmem都是基于内存的文件系统。这里你先不用管,基于内存的文件系统具体是怎么回事儿。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static const struct inode_operations ramfs_dir_inode_operations = {
|
|
|
|
|
......
|
|
|
|
|
.mknod = ramfs_mknod,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static const struct inode_operations shmem_dir_inode_operations = {
|
|
|
|
|
#ifdef CONFIG_TMPFS
|
|
|
|
|
......
|
|
|
|
|
.mknod = shmem_mknod,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这两个mknod虽然实现不同,但是都会调用到同一个函数init\_special\_inode。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
|
|
|
|
|
{
|
|
|
|
|
inode->i_mode = mode;
|
|
|
|
|
if (S_ISCHR(mode)) {
|
|
|
|
|
inode->i_fop = &def_chr_fops;
|
|
|
|
|
inode->i_rdev = rdev;
|
|
|
|
|
} else if (S_ISBLK(mode)) {
|
|
|
|
|
inode->i_fop = &def_blk_fops;
|
|
|
|
|
inode->i_rdev = rdev;
|
|
|
|
|
} else if (S_ISFIFO(mode))
|
|
|
|
|
inode->i_fop = &pipefifo_fops;
|
|
|
|
|
else if (S_ISSOCK(mode))
|
|
|
|
|
; /* leave it no_open_fops */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
显然这个文件是个特殊文件,inode也是特殊的。这里这个inode可以关联字符设备、块设备、FIFO文件、Socket等。我们这里只看字符设备。
|
|
|
|
|
|
|
|
|
|
这里的inode的file\_operations指向一个def\_chr\_fops,这里面只有一个open,就等着你打开它。
|
|
|
|
|
|
|
|
|
|
另外,inode的i\_rdev指向这个设备的dev\_t。还记得cdev\_map吗?通过这个dev\_t,可以找到我们刚在加载的字符设备cdev。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
const struct file_operations def_chr_fops = {
|
|
|
|
|
.open = chrdev_open,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
到目前为止,我们只是创建了/dev下面的一个文件,并且和相应的设备号关联起来。但是,我们还没有打开这个/dev下面的设备文件。
|
|
|
|
|
|
|
|
|
|
现在我们来打开它。打开一个文件的流程,我们在[文件系统](https://time.geekbang.org/column/article/97876)那一节讲过了,这里不再重复。最终就像打开字符设备的图中一样,打开文件的进程的task\_struct里,有一个数组代表它打开的文件,下标就是文件描述符fd,每一个打开的文件都有一个struct file结构,会指向一个dentry项。dentry可以用来关联inode。这个dentry就是咱们上面mknod的时候创建的。
|
|
|
|
|
|
|
|
|
|
在进程里面调用open函数,最终会调用到这个特殊的inode的open函数,也就是chrdev\_open。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static int chrdev_open(struct inode *inode, struct file *filp)
|
|
|
|
|
{
|
|
|
|
|
const struct file_operations *fops;
|
|
|
|
|
struct cdev *p;
|
|
|
|
|
struct cdev *new = NULL;
|
|
|
|
|
int ret = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p = inode->i_cdev;
|
|
|
|
|
if (!p) {
|
|
|
|
|
struct kobject *kobj;
|
|
|
|
|
int idx;
|
|
|
|
|
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
|
|
|
|
|
new = container_of(kobj, struct cdev, kobj);
|
|
|
|
|
p = inode->i_cdev;
|
|
|
|
|
if (!p) {
|
|
|
|
|
inode->i_cdev = p = new;
|
|
|
|
|
list_add(&inode->i_devices, &p->list);
|
|
|
|
|
new = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
fops = fops_get(p->ops);
|
|
|
|
|
......
|
|
|
|
|
replace_fops(filp, fops);
|
|
|
|
|
if (filp->f_op->open) {
|
|
|
|
|
ret = filp->f_op->open(inode, filp);
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在这个函数里面,我们首先看这个inode的i\_cdev,是否已经关联到cdev。如果第一次打开,当然没有。没有没关系,inode里面有i\_rdev呀,也就是有dev\_t。我们可以通过它在cdev\_map中找cdev。咱们上面注册过了,所以肯定能够找到。找到后我们就将inode的i\_cdev,关联到找到的cdev new。
|
|
|
|
|
|
|
|
|
|
找到cdev就好办了。cdev里面有file\_operations,这是设备驱动程序自己定义的。我们可以通过它来操作设备驱动程序,把它付给struct file里面的file\_operations。这样以后操作文件描述符,就是直接操作设备了。
|
|
|
|
|
|
|
|
|
|
最后,我们需要调用设备驱动程序的file\_operations的open函数,真正打开设备。对于打印机,调用的是lp\_open。对于鼠标调用的是input\_proc\_devices\_open,最终会调用到logibm\_open。这些多和设备相关,你不必看懂它们。
|
|
|
|
|
|
|
|
|
|
## 写入字符设备
|
|
|
|
|
|
|
|
|
|
当我们像打开一个文件一样打开一个字符设备之后,接下来就是对这个设备的读写。对于文件的读写咱们在文件系统那一章详细讲述过,读写的过程是类似的,所以这里我们只解析打印机驱动写入的过程。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/9b/e2/9bd3cd8a8705dbf69f889ba3b2b5c2e2.jpeg)
|
|
|
|
|
|
|
|
|
|
写入一个字符设备,就是用文件系统的标准接口write,参数文件描述符fd,在内核里面调用的sys\_write,在sys\_write里面根据文件描述符fd得到struct file结构。接下来再调用vfs\_write。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos)
|
|
|
|
|
{
|
|
|
|
|
if (file->f_op->write)
|
|
|
|
|
return file->f_op->write(file, p, count, pos);
|
|
|
|
|
else if (file->f_op->write_iter)
|
|
|
|
|
return new_sync_write(file, p, count, pos);
|
|
|
|
|
else
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们可以看到,在\_\_vfs\_write里面,我们会调用struct file结构里的file\_operations的write函数。上面我们打开字符设备的时候,已经将struct file结构里面的file\_operations指向了设备驱动程序的file\_operations结构,所以这里的write函数最终会调用到lp\_write。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static ssize_t lp_write(struct file * file, const char __user * buf,
|
|
|
|
|
size_t count, loff_t *ppos)
|
|
|
|
|
{
|
|
|
|
|
unsigned int minor = iminor(file_inode(file));
|
|
|
|
|
struct parport *port = lp_table[minor].dev->port;
|
|
|
|
|
char *kbuf = lp_table[minor].lp_buffer;
|
|
|
|
|
ssize_t retv = 0;
|
|
|
|
|
ssize_t written;
|
|
|
|
|
size_t copy_size = count;
|
|
|
|
|
......
|
|
|
|
|
/* Need to copy the data from user-space. */
|
|
|
|
|
if (copy_size > LP_BUFFER_SIZE)
|
|
|
|
|
copy_size = LP_BUFFER_SIZE;
|
|
|
|
|
......
|
|
|
|
|
if (copy_from_user (kbuf, buf, copy_size)) {
|
|
|
|
|
retv = -EFAULT;
|
|
|
|
|
goto out_unlock;
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
do {
|
|
|
|
|
/* Write the data. */
|
|
|
|
|
written = parport_write (port, kbuf, copy_size);
|
|
|
|
|
if (written > 0) {
|
|
|
|
|
copy_size -= written;
|
|
|
|
|
count -= written;
|
|
|
|
|
buf += written;
|
|
|
|
|
retv += written;
|
|
|
|
|
}
|
|
|
|
|
......
|
|
|
|
|
if (need_resched())
|
|
|
|
|
schedule ();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (count) {
|
|
|
|
|
copy_size = count;
|
|
|
|
|
if (copy_size > LP_BUFFER_SIZE)
|
|
|
|
|
copy_size = LP_BUFFER_SIZE;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (copy_from_user(kbuf, buf, copy_size)) {
|
|
|
|
|
if (retv == 0)
|
|
|
|
|
retv = -EFAULT;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} while (count > 0);
|
|
|
|
|
......
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这个设备驱动程序的写入函数的实现还是比较典型的。先是调用copy\_from\_user将数据从用户态拷贝到内核态的缓存中,然后调用parport\_write写入外部设备。这里还有一个schedule函数,也即写入的过程中,给其他线程抢占CPU的机会。然后,如果count还是大于0,也就是数据还没有写完,那我们就接着copy\_from\_user,接着parport\_write,直到写完为止。
|
|
|
|
|
|
|
|
|
|
## 使用IOCTL控制设备
|
|
|
|
|
|
|
|
|
|
对于I/O设备来讲,我们前面也说过,除了读写设备,还会调用ioctl,做一些特殊的I/O操作。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/c3/1d/c3498dad4f15712529354e0fa123c31d.jpeg)
|
|
|
|
|
|
|
|
|
|
ioctl也是一个系统调用,它在内核里面的定义如下:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
|
|
|
|
|
{
|
|
|
|
|
int error;
|
|
|
|
|
struct fd f = fdget(fd);
|
|
|
|
|
......
|
|
|
|
|
error = do_vfs_ioctl(f.file, fd, cmd, arg);
|
|
|
|
|
fdput(f);
|
|
|
|
|
return error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
其中,fd是这个设备的文件描述符,cmd是传给这个设备的命令,arg是命令的参数。其中,对于命令和命令的参数,使用ioctl系统调用的用户和驱动程序的开发人员约定好行为即可。
|
|
|
|
|
|
|
|
|
|
其实cmd看起来是一个int,其实他的组成比较复杂,它由几部分组成:
|
|
|
|
|
|
|
|
|
|
* 最低八位为NR,是命令号;
|
|
|
|
|
* 然后八位是TYPE,是类型;
|
|
|
|
|
* 然后十四位是参数的大小;
|
|
|
|
|
* 最高两位是DIR,是方向,表示写入、读出,还是读写。
|
|
|
|
|
|
|
|
|
|
由于组成比较复杂,有一些宏是专门用于组成这个cmd值的。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/*
|
|
|
|
|
* Used to create numbers.
|
|
|
|
|
*/
|
|
|
|
|
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
|
|
|
|
|
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
|
|
|
|
|
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
|
|
|
|
|
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* used to decode ioctl numbers.. */
|
|
|
|
|
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
|
|
|
|
|
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
|
|
|
|
|
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
|
|
|
|
|
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在用户程序中,可以通过上面的“Used to create numbers”这些宏,根据参数生成cmd,在驱动程序中,可以通过下面的“used to decode ioctl numbers”这些宏,解析cmd后,执行指令。
|
|
|
|
|
|
|
|
|
|
ioctl中会调用do\_vfs\_ioctl,这里面对于已经定义好的cmd,进行相应的处理。如果不是默认定义好的cmd,则执行默认操作。对于普通文件,调用file\_ioctl;对于其他文件调用vfs\_ioctl。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
|
|
|
|
|
unsigned long arg)
|
|
|
|
|
{
|
|
|
|
|
int error = 0;
|
|
|
|
|
int __user *argp = (int __user *)arg;
|
|
|
|
|
struct inode *inode = file_inode(filp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch (cmd) {
|
|
|
|
|
......
|
|
|
|
|
case FIONBIO:
|
|
|
|
|
error = ioctl_fionbio(filp, argp);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case FIOASYNC:
|
|
|
|
|
error = ioctl_fioasync(fd, filp, argp);
|
|
|
|
|
break;
|
|
|
|
|
......
|
|
|
|
|
case FICLONE:
|
|
|
|
|
return ioctl_file_clone(filp, arg, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
if (S_ISREG(inode->i_mode))
|
|
|
|
|
error = file_ioctl(filp, cmd, arg);
|
|
|
|
|
else
|
|
|
|
|
error = vfs_ioctl(filp, cmd, arg);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return error;
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
由于咱们这里是设备驱动程序,所以调用的是vfs\_ioctl。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/**
|
|
|
|
|
* vfs_ioctl - call filesystem specific ioctl methods
|
|
|
|
|
* @filp: open file to invoke ioctl method on
|
|
|
|
|
* @cmd: ioctl command to execute
|
|
|
|
|
* @arg: command-specific argument for ioctl
|
|
|
|
|
*
|
|
|
|
|
* Invokes filesystem specific ->unlocked_ioctl, if one exists; otherwise
|
|
|
|
|
* returns -ENOTTY.
|
|
|
|
|
*
|
|
|
|
|
* Returns 0 on success, -errno on error.
|
|
|
|
|
*/
|
|
|
|
|
long vfs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
|
|
|
|
|
{
|
|
|
|
|
int error = -ENOTTY;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!filp->f_op->unlocked_ioctl)
|
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
|
|
|
|
|
if (error == -ENOIOCTLCMD)
|
|
|
|
|
error = -ENOTTY;
|
|
|
|
|
out:
|
|
|
|
|
return error;
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这里面调用的是struct file里file\_operations的unlocked\_ioctl函数。我们前面初始化设备驱动的时候,已经将file\_operations指向设备驱动的file\_operations了。这里调用的是设备驱动的unlocked\_ioctl。对于打印机程序来讲,调用的是lp\_ioctl。可以看出来,这里面就是switch语句,它会根据不同的cmd,做不同的操作。
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
static long lp_ioctl(struct file *file, unsigned int cmd,
|
|
|
|
|
unsigned long arg)
|
|
|
|
|
{
|
|
|
|
|
unsigned int minor;
|
|
|
|
|
struct timeval par_timeout;
|
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
minor = iminor(file_inode(file));
|
|
|
|
|
mutex_lock(&lp_mutex);
|
|
|
|
|
switch (cmd) {
|
|
|
|
|
......
|
|
|
|
|
default:
|
|
|
|
|
ret = lp_do_ioctl(minor, cmd, arg, (void __user *)arg);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
mutex_unlock(&lp_mutex);
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static int lp_do_ioctl(unsigned int minor, unsigned int cmd,
|
|
|
|
|
unsigned long arg, void __user *argp)
|
|
|
|
|
{
|
|
|
|
|
int status;
|
|
|
|
|
int retval = 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch ( cmd ) {
|
|
|
|
|
case LPTIME:
|
|
|
|
|
if (arg > UINT_MAX / HZ)
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
LP_TIME(minor) = arg * HZ/100;
|
|
|
|
|
break;
|
|
|
|
|
case LPCHAR:
|
|
|
|
|
LP_CHAR(minor) = arg;
|
|
|
|
|
break;
|
|
|
|
|
case LPABORT:
|
|
|
|
|
if (arg)
|
|
|
|
|
LP_F(minor) |= LP_ABORT;
|
|
|
|
|
else
|
|
|
|
|
LP_F(minor) &= ~LP_ABORT;
|
|
|
|
|
break;
|
|
|
|
|
case LPABORTOPEN:
|
|
|
|
|
if (arg)
|
|
|
|
|
LP_F(minor) |= LP_ABORTOPEN;
|
|
|
|
|
else
|
|
|
|
|
LP_F(minor) &= ~LP_ABORTOPEN;
|
|
|
|
|
break;
|
|
|
|
|
case LPCAREFUL:
|
|
|
|
|
if (arg)
|
|
|
|
|
LP_F(minor) |= LP_CAREFUL;
|
|
|
|
|
else
|
|
|
|
|
LP_F(minor) &= ~LP_CAREFUL;
|
|
|
|
|
break;
|
|
|
|
|
case LPWAIT:
|
|
|
|
|
LP_WAIT(minor) = arg;
|
|
|
|
|
break;
|
|
|
|
|
case LPSETIRQ:
|
|
|
|
|
return -EINVAL;
|
|
|
|
|
break;
|
|
|
|
|
case LPGETIRQ:
|
|
|
|
|
if (copy_to_user(argp, &LP_IRQ(minor),
|
|
|
|
|
sizeof(int)))
|
|
|
|
|
return -EFAULT;
|
|
|
|
|
break;
|
|
|
|
|
case LPGETSTATUS:
|
|
|
|
|
if (mutex_lock_interruptible(&lp_table[minor].port_mutex))
|
|
|
|
|
return -EINTR;
|
|
|
|
|
lp_claim_parport_or_block (&lp_table[minor]);
|
|
|
|
|
status = r_str(minor);
|
|
|
|
|
lp_release_parport (&lp_table[minor]);
|
|
|
|
|
mutex_unlock(&lp_table[minor].port_mutex);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (copy_to_user(argp, &status, sizeof(int)))
|
|
|
|
|
return -EFAULT;
|
|
|
|
|
break;
|
|
|
|
|
case LPRESET:
|
|
|
|
|
lp_reset(minor);
|
|
|
|
|
break;
|
|
|
|
|
case LPGETFLAGS:
|
|
|
|
|
status = LP_F(minor);
|
|
|
|
|
if (copy_to_user(argp, &status, sizeof(int)))
|
|
|
|
|
return -EFAULT;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
retval = -EINVAL;
|
|
|
|
|
}
|
|
|
|
|
return retval
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 总结时刻
|
|
|
|
|
|
|
|
|
|
这一节我们讲了字符设备的打开、写入和ioctl等最常见的操作。一个字符设备要能够工作,需要三部分配合。
|
|
|
|
|
|
|
|
|
|
第一,有一个设备驱动程序的ko模块,里面有模块初始化函数、中断处理函数、设备操作函数。这里面封装了对于外部设备的操作。加载设备驱动程序模块的时候,模块初始化函数会被调用。在内核维护所有字符设备驱动的数据结构cdev\_map里面注册,我们就可以很容易根据设备号,找到相应的设备驱动程序。
|
|
|
|
|
|
|
|
|
|
第二,在/dev目录下有一个文件表示这个设备,这个文件在特殊的devtmpfs文件系统上,因而也有相应的dentry和inode。这里的inode是一个特殊的inode,里面有设备号。通过它,我们可以在cdev\_map中找到设备驱动程序,里面还有针对字符设备文件的默认操作def\_chr\_fops。
|
|
|
|
|
|
|
|
|
|
第三,打开一个字符设备文件和打开一个普通的文件有类似的数据结构,有文件描述符、有struct file、指向字符设备文件的dentry和inode。字符设备文件的相关操作file\_operations一开始指向def\_chr\_fops,在调用def\_chr\_fops里面的chrdev\_open函数的时候,修改为指向设备操作函数,从而读写一个字符设备文件就会直接变成读写外部设备了。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/fb/cd/fba61fe95e0d2746235b1070eb4c18cd.jpeg)
|
|
|
|
|
|
|
|
|
|
## 课堂练习
|
|
|
|
|
|
|
|
|
|
这节我用打印机驱动程序作为例子来给你讲解字符设备,请你仔细看一下它的代码,设想一下,如果让你自己写一个字符设备驱动程序,应该实现哪些函数呢?
|
|
|
|
|
|
|
|
|
|
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)
|
|
|
|
|
|