注:LDD3的整个第三章都在介绍这个程序。也可以看官方的,应该比我写的好。:)
1 代码基本结构
scull 使用一块虚拟内存作为模拟设备,并将这块内存划分成多个设备。例如,程序中可以定义 4 个 scull 设备,每个设备都有一个独立的内存区域。每个区域的大小是通过编译时参数或模块参数指定的。
内存分配: scull 的内存分配是通过一个链表实现的,每个链表节点保存了一块内存数据。链表中每个节点的大小是固定的,称为 quantum,多个 quantum 组成一个 qset。当数据超过当前节点的 quantum 大小时,scull 会自动分配新的节点并将其连接到链表中,形成一个灵活的内存管理机制。
核心结构
struct scull_dev
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 */
struct semaphore sem; /* Mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
scull_qset
struct scull_qset {
void **data;
struct scull_qset *next;
};
注册的字符驱动结构体
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.unlocked_ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
2 基本流程
scull_init_module
这个函数是初始化的函数,
int scull_init_module(void)
{
int result, i;
dev_t dev = 0;
/*
* Get a range of minor numbers to work with, asking for a dynamic
* major unless directed otherwise at load time.
*/
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;
}
/*
* allocate the devices -- we can't have them static, as the number
* can be specified at load time
*/
scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
if (!scull_devices) {
result = -ENOMEM;
goto fail; /* Make this more graceful */
}
memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));
/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
mutex_init(&scull_devices[i].lock);
scull_setup_cdev(&scull_devices[i], i);
}
/* At this point call the init function for any friend device */
dev = MKDEV(scull_major, scull_minor + scull_nr_devs);
dev += scull_p_init(dev);
dev += scull_access_init(dev);
#ifdef SCULL_DEBUG /* only when debugging */
scull_create_proc();
#endif
return 0; /* succeed */
fail:
scull_cleanup_module();
return result;
}
可以看到,初始化的操作就是几个,alloc_chrdev_region,这个用来动态注册设备,kmalloc,这个给自己的主结构分配内存,然后初始化了四个次设备(TODO,这部分还要看看)。另外两个友设备这次就先不看了。。
scull_cleanup_module
这个是退出的清理函数
/*
* The cleanup function is used to handle initialization failures as well.
* Thefore, it must be careful to work correctly even if some of the items
* have not been initialized
*/
void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(scull_major, scull_minor);
/* Get rid of our char dev entries */
if (scull_devices) {
for (i = 0; i < scull_nr_devs; i++) {
scull_trim(scull_devices + i);
cdev_del(&scull_devices[i].cdev);
}
kfree(scull_devices);
}
#ifdef SCULL_DEBUG /* use proc only if debugging */
scull_remove_proc();
#endif
/* cleanup_module is never called if registering failed */
unregister_chrdev_region(devno, scull_nr_devs);
/* and call the cleanup functions for friend devices */
scull_p_cleanup();
scull_access_cleanup();
}
然后就是注册回调:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.unlocked_ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};
scull_trim():
会在scull_cleanup_module,open的时候如果是只读模式也会调用,这个函数会遍历链表并释放所有分配的内存。
scull_open():
打开设备。 在设备文件被打开时调用,增加设备的引用计数,并进行必要的初始化。
scull_release():
释放设备。 当设备文件被关闭时调用,减少引用计数,如果没有进程再使用该设备,则释放其资源。
scull_read():
从设备读取数据。 将内存中的数据拷贝到用户空间。
scull_write():
向设备写入数据。 将用户空间的数据拷贝到设备的内存中,必要时扩展内存区域。
scull_follow():
返回指定的链表位置指针,同时如果不够,会分配新的链表空间。
试了一下对ko进行echo和cat,有一点小发现。不管是echo还是cat,都调用了一串注册的回调。
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ echo "Hello, SCULL!" > /dev/scull0
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ dmesg | tail -n 10
[1522160.238474] scull: get major 0 0
[1522160.239150] scull: get result 241
[1522160.239687] scullsingle registered at f100008
[1522160.239688] sculluid registered at f100009
[1522160.239689] scullwuid registered at f10000a
[1522160.239689] scullpriv registered at f10000b
[1522182.942577] scull_open:
[1522182.943011] scull_trim:
[1522182.943345] scull_write:
[1522182.943672] scull_follow:
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ cat /dev/scull0
Hello, SCULL!
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ dmesg | tail -n 10
[1522160.239689] scullwuid registered at f10000a
[1522160.239689] scullpriv registered at f10000b
[1522182.942577] scull_open:
[1522182.943011] scull_trim:
[1522182.943345] scull_write:
[1522182.943672] scull_follow:
[1522205.620598] scull_open:
[1522205.621056] scull_read:
[1522205.621383] scull_follow:
[1522205.621738] scull_read:
按照规范来说,open的时候才会打开scull_open,那么对于echo和cat,详细要看看这两个命令的代码才行。估计这个本身就是执行的一串操作。这里follow和trim是自定义操作。所以
命令行echo输入:open->write
命令行cat输出:open->read
3 重点函数
然后重点看一下write和read两个函数:
scull_write
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr;
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset;
int item, s_pos, q_pos, rest;
ssize_t retval = -ENOMEM; /* value used in "goto out" statements */
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
/* find listitem, qset index and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
/* follow the list up to the right position */
dptr = scull_follow(dev, item);
if (dptr == NULL)
goto out;
if (!dptr->data) {
dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
if (!dptr->data)
goto out;
memset(dptr->data, 0, qset * sizeof(char *));
}
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
/* write only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
/* update the size */
if (dev->size < *f_pos)
dev->size = *f_pos;
out:
mutex_unlock(&dev->lock);
return retval;
}
在这里也可以看到内存的分配,大概是一个1000*4000的矩阵。有1000条,每条是4K。每次的4K是动态分配的。整个4000K也就是4M作为一个item,也就是数据结构中的链表长度。
| ...4000 | |||||||||
| ...1000 |
完成之后,将数据的长度写回给驱动中的size。没写完会继续调用scull_write。
这里的锁保护是进来的时候会mutex_lock_interruptible(&dev->lock),结束的时候mutex_unlock(&dev->lock);
scull_read
ssize_t scull_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
struct scull_dev *dev = filp->private_data;
struct scull_qset *dptr; /* the first listitem */
int quantum = dev->quantum, qset = dev->qset;
int itemsize = quantum * qset; /* how many bytes in the listitem */
int item, s_pos, q_pos, rest;
ssize_t retval = 0;
if (mutex_lock_interruptible(&dev->lock))
return -ERESTARTSYS;
if (*f_pos >= dev->size)
goto out;
if (*f_pos + count > dev->size)
count = dev->size - *f_pos;
/* find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
/* follow the list up to the right position (defined elsewhere) */
dptr = scull_follow(dev, item);
if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
goto out; /* don't fill holes */
/* read only up to the end of this quantum */
if (count > quantum - q_pos)
count = quantum - q_pos;
if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
retval = -EFAULT;
goto out;
}
*f_pos += count;
retval = count;
out:
mutex_unlock(&dev->lock);
return retval;
}
Read的流程基本就是write的反操作。可以再说一下寻址:
/* find listitem, qset index, and offset in the quantum */
item = (long)*f_pos / itemsize;
rest = (long)*f_pos % itemsize;
s_pos = rest / quantum; q_pos = rest % quantum;
就是通过这个来查找两级的地址,第一级是item,第二级就是里面的数组。之后就是根据偏移找到正确的数据,并拷贝到用户空间。锁的操作和write一样。
最后就是scull_ioctl。
命令的定义如下:
#define SCULL_IOCRESET _IO(SCULL_IOC_MAGIC, 0)
/*
* S means "Set" through a ptr,
* T means "Tell" directly with the argument value
* G means "Get": reply by setting through a pointer
* Q means "Query": response is on the return value
* X means "eXchange": switch G and S atomically
* H means "sHift": switch T and Q atomically
*/
#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC, 1, int)
#define SCULL_IOCSQSET _IOW(SCULL_IOC_MAGIC, 2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC, 3)
#define SCULL_IOCTQSET _IO(SCULL_IOC_MAGIC, 4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC, 5, int)
#define SCULL_IOCGQSET _IOR(SCULL_IOC_MAGIC, 6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC, 7)
#define SCULL_IOCQQSET _IO(SCULL_IOC_MAGIC, 8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC, 11)
#define SCULL_IOCHQSET _IO(SCULL_IOC_MAGIC, 12)
比较特别的是,设置代码中会判断是否是admin,也就是要sudo才能设置。
if (! capable (CAP_SYS_ADMIN))
return -EPERM;
这部分的代码主要还是设置之前说的scull_quantum和scull_qset。这里用了好几种方法,直接返回,或者用指针参数返回,还有就是设置并返回旧值。大概就是这几种。。。
最后一个就是proc的调试,打开SCULL_DEBUG调试宏。在proc下面会创建两个节点,scullmem,scullseq。
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ ls /proc/scull*
/proc/scullmem /proc/scullpipe /proc/scullseq
这里是通过scull_read_procmem和scull_seq_show来返回的。如下:
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ cat /proc/scullmem
Device 0: qset 1000, q 10, sz 14
item at 00000000dd711a31, qset at 0000000002f9ee99
0: aa623c79
1: 601ed2c8
Device 1: qset 1000, q 10, sz 0
Device 2: qset 1000, q 10, sz 0
Device 3: qset 1000, q 10, sz 0
ubuntu@VM-8-10-ubuntu:~/ldd3/scull$ cat /proc/scullseq
Device 0: qset 1000, q 10, sz 14
item at 00000000dd711a31, qset at 0000000002f9ee99
0: aa623c79
1: 601ed2c8
Device 1: qset 1000, q 10, sz 0
Device 2: qset 1000, q 10, sz 0
Device 3: qset 1000, q 10, sz 0
一个是扫内存,一个是把链表打印一圈。结果看起来都差不多。。
4 小结
大部分代码都是八股文。其中一些细节还是挺周到的,比如权限,多种IOCTL。此外对于内存的管理比较花心思,看来这部分是重点了。。。
最后说一下LDD这本书,当然,不否认这个是经典,但是我觉得编排上真不如Beginning Linux Programming,Linux程序设计那本。那本书浅显易懂,循序渐进。我以前就是学了那本书,一直吃了10年老本。。比起来LDD就逊色不少。


2587

被折叠的 条评论
为什么被折叠?



