LDD3学习5--Scull代码流程

注: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就逊色不少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值