Linux设备驱动程序学习(三)——字符设备驱动程序

   前面我们了解了Linux设备驱动程序的入门知识和准备工作,下面我们就将真正的进入Linux设备驱动程序的世界,下面就将LDD3下面的scull字符设备驱动程序的实现来学习字符设备驱动的特点以及如何在用户空间被调用,书籍和源码下载地址为链接: https://pan.baidu.com/s/1OB4z_vjlov2gHTwrCe7hjQ 提取码: b427

字符设备驱动程序

字符设备和字符设备驱动程序

   字符设备和字符设备驱动程序是两个不同的概念
   字符设备就是以字节为单位进行顺序访问的一类设备的总称。典型的常用的字符设备有:键盘,串口,控制台等。字符设备驱动程序就是提供操作字符设备的机制。

scull的设计

   scull( Simple Character Utility for Loading Localities):区域装载的简单字符工具。scull是一个操作内存区域的简单字符设备驱动程序,这片内存区域就相当于一个设备。scull的优点在于它不依赖硬件,scull 只是操作从内核分配的一些内存。
   编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制)。这一部分的目的就是通过scull源代码实现scull0-scull3这四个字符设备,这4 个设备分别由一个全局、持久的内存区组成。全局意味着如果设备被多次打开, 设备中含有的数据由所有打开它的文件描述符共享. 持久意味着如果设备关闭又重新打开, 数据不会丢失。可以用常用的命令来存取和测试, 例如 cp, cat, 以及 I/O 重定向。

主设备号和次设备号

在Linux系统上输入:ls -l /dev观察输出。我们会发现如下面所示的文件的详细信息:

  crw-r--r--  1 root root        4,   0  6月 26 2010 systty  
  crw-rw-rw-  1 root tty         5,   0  6月 26 2010 tty  
  crw--w----  1 root root        4,   0  6月 26 2010 tty0  
  crw--w----  1 root root        4,   1  6月 26 2010 tty1  
  crw--w----  1 root tty         4,  10  6月 26 2010 tty10  
  crw--w----  1 root tty         4,  11  6月 26 2010 tty11   

   这是系统输出的一部分信息,我们对字符设备的访问是通过文件系统内的设备名称进行的,内核把设备当作特殊的文件或者说是设备文件来进行操作。
   它们通常位于/dev目录下。crw-rw-rw- 这一部分是文件权限等方面的说明。第一项的“c"标识它是一个字符设备文件,可能你会看到其他的文件是"b" "l"字样,它们代表不同的设备文件类型。
   文件拥有者和拥有组后面两个数字就是设备的主设备号和次设备号。例如4、5都是主设备号,0 、1 、10 、11等都是次设备号。

   主设备号用来标识设备对应的驱动程序;次设备号用于正确确定设备文件对应的设备。例如我们要操作某个设备,首先,我们要知道设备在/dev下的设备文件名。这个设备文件提供主设备号以及次设备号。然后内核通过设备文件提供的主设备找到设备驱动程序(操作设备由驱动程序实现)。最后通过主设备号和次设备构成的设备号找到正确的设备。有了操作的对象(设备)和操作的方法(驱动程序)那就可以完成了我们的要求。
   一个驱动程序可以操作多个设备,所以不同的设备可以具有相同的主设备号。因为我们在添加设备到内核的时候我们是关联设备号的,不同的设备可以具有相同的主设备号,那不同的次设备号和相同的主设备号结合就可以构成不同的设备号了,就标识了不同的设备了。等到理解了设备的注册后再来理解这个应该比较容易。但确实只能是不同的设备号才能标识不同的设备。假如一个设备的的主设备是4次设备是5,另一个设备主设备是5次设备是5,那不能说这两个是同一个设备吧。只能综合主设备号和次设备号才可以的嘛。

设备编号的内部表达

   在上面主设备和次设备的介绍中我们提到设备编号,真正能标识不同的设备的是设备编号,每一个设备有一个唯一的设备编号
   在内核中,用dev_t类型来保存设备编号,它是一个32位的数,其中前12位用来表示主设备号,后20位用来表示次设备号。这个类型在<linux/types.h>中定义。

   设备号由主设备号和次设备号构成。内核提供三个宏来实现这三个东西的转换。分别是:

MKDEV(int major, int minor)    //将主次设备号转换成dev_t类型
MAJOR(dev_tdev)            //获得dev_tdev中的主设备号
MINOR(dev_t dev)//获得dev_tdev中的次设备号

   这三个宏在<linux/kde_t.h>中定义。

分配和释放设备编号

   内核是通过设备编号找到设备的,理所当然地要建立一个字符设备那必须要获得字符设备编号。要建立多少个字符设备就要得到多少个字符设备编号。
   完成这一工作有两种方式,一种是静态获取,一种是动态获取,分别由:register_chrdev_region() 和alloc_chrdev_region()这两个函数实现。成功调用申请设备编号的函数后,在系统的/proc/devices下就会包含设备以及设备主设备号的信息。函数在<linux/fs.h>中声明。字符设备不再使用时应该释放它们占用的编号。由unregister_chrdev_region()。这三个函数的原型如下:
   静态获取设备编号

int register_chrdev_region(dev_t first, unsigned int count, char *name); 
返回: 0  成功分配.返回:负数  分配失败. 
first 是你要分配的设备编号的起始范围值. first 的次编号部分常常是 0, 但是没有特别要求. 
count 是你请求的连续设备编号的总数. 注意, 如果 count 太大, 那么所请求的范围可能和下一个主设备号重叠, 但是只要请求的编号范围可用, 一切都仍然会正确工作.。
name 是应当连接到这个编号范围的设备的名字; 

   动态获取设备编号:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,  unsigned int count, char *name); 
dev 是一个只用于输出的参数, 在成功完成调用后将保存已分配范围的第一个编号. fisetminor 是要使用的被请求的第一个次设备号,它常常是 0. 
count 和 name 参数如同给 request_chrdev_region 函数的一样。

   成功调用申请设备编号的函数后,在系统的/proc/devices下就会包含设备以及设备主设备号的信息。

   不管采用哪种方式分配设备编号, 当不再使用它们时释放它,都要调用设备编号释放函数:
   void unregister_chrdev_region(dev_t first, unsigned int count);
   这三个函数在<linux/fs.h>中声明。

  • 静态分配: 即直接使用内核源码数的Documentation/devices.txt中定义的未使用的主设备号。它对应于register_chrdev_region函数。
  • 动态分配,即动态分配一个主设备号.它对应于alloc_chrdev_region函数.
    尽量使用动态分配的方法, 这样就能在加载甚至编译模块的时候设定主设备号,大大优于静态分配。

   动态分配的缺点: 无法在分配设备号之前创造设备节点, 但一旦设备号被分配之后, 你可以通过/proc/devices来读取它,也可以通过sysfs来获取更多的设备信息。

字符设备程序的实现

   完成一个字符设备程序的编写是这一部分的重点,因此在这就创建一个scull字符设备程序的过程来学习字符设备程序。

申请分配scull设备编号

   实现一个scull字符设备程序,第一步就是要先给字符设备分配设备编号,这里需要创建4个scull字符设备scull0-scull4,根据源码来分析设备编号的分配:

  int result, i;
  dev_t dev = 0;    //设备编号
/*
*申请分配设备编号,根据scull_major的值是否为0,分别采用静态分配设备编号(register_chrdev_region)
*或动态分配设备编号(alloc_chrdev_region)的方法。scull_major代表主设备号,它的值是怎么确定的呢?
*scull_init_module函数中,如果用户没有通过命令行参数给scull_major赋任意大于0的值,
*则会采用alloc_chrdev_region动态分配设备编号。如果用户给scull_major赋了一个大于0值,
*则采用register_chrdev_region静态申请设备编号。(因此这里默认动态分配)
*/
  if (scull_major) {               //scull_major在头文件中定义为0
  	dev = MKDEV(scull_major, scull_minor);    //将主次设备号转换成dev_t类型  
  	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;
  }

给scull设备分配内存空间

   既然给scull设备分配好了主次设备号,那么下一步就是给设备分配内存空间,根据源代码可以分析:

   /* 
	 *给scull_nr_devs个scull设备分配内存空间,并将分配得到的内存清0。
	 *scull_nr_devs默认值为4,即默认创建4个scull设备(scull0 - scull3),
	 *每个scull设备由scull_dev结构体表示,其定义在scull.h文件中
	 */
	scull_devices = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);
	if (!scull_devices) {      //判断是否分配成功
		result = -ENOMEM;
		goto fail;     	
}
	memset(scull_devices, 0, scull_nr_devs * sizeof(struct scull_dev));  //内存清0


struct scull_dev {
   struct scull_qset *data;     /* 指向第一个量子集的指针 */
   int quantum;             /* 当前量子的大小 */
   int qset;                 /* 当前数组的大小 */
   unsigned long size;        /* 保存在其中的数据总量 */
   unsigned int access_key;   /* 被 sculluid 和scullpriv 使用*/
   struct semaphore sem;    /* 互斥信号量 */
   struct cdev cdev;         /* 字符设备结构 */
};

scull设备的注册

   前面实现了scull设备的设备号分配以及scull设备的内存分配,那么最后一步就是实现scull设备的注册,只要完成了这一步,那么4个scull设备就初始化成功了,源码及分析为:

 /* 对前面创建scull设备进行初始化,共循环scull_nr_devs次,每次循环完成对一个scull设备的初始化。 */
 /*scull_devices[i].quantum代表scull设备当前“量子”大小,这里赋值为scull_quantum,其默认值为4000。
  *scull_devices[i].qset代表当前数组大小,这里赋值为scull_qset,其默认值为1000。
  *调用init_MUTEX对每个scull设备的sem成员进行初始化,这是一个互斥体,用于保证对scull设备的互斥访问
  *在每次循环的最后,调用了scull_setup_cdev函数对相应scull设备进行设置
    */
 for (i = 0; i < scull_nr_devs; i++) {
 	scull_devices[i].quantum = scull_quantum;
 	scull_devices[i].qset = scull_qset;
 	init_MUTEX(&scull_devices[i].sem);
 	scull_setup_cdev(&scull_devices[i], i);  //调用scull_setup_cdev函数对相应scull设备进行设置
 }
/*
*scull_setup_cdev函数完成对scull设备的cdev成员变量(struct cdev类型)的初始化和注册,即注册设备节点。
*cdev结构体在内核中代表一个字符设备
struct cdev { 
 struct kobject kobj;                  //内嵌的内核对象.
 struct module *owner;                 //该字符设备所在的内核模块的对象指针.
 const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
 struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
 dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
 unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};
*/
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
 int err, devno = MKDEV(scull_major, scull_minor + index);    //调用MKDEV宏得到设备编号,注意,4个scull设备的主设备号都是一样的,但次设备号分别是0 - 3。  
 cdev_init(&dev->cdev, &scull_fops);  
 //调用cdev_init函数对cdev结构体进行初始化,指定对应的文件操作函数集是scull_fops,这个scull_fops必须是前面已经定义实现好的。
 dev->cdev.owner = THIS_MODULE;    //指定所有者是THIS_MODULE
 dev->cdev.ops = &scull_fops;      //多余的,因为在前面已经指定了文件操作函数集是scull_fops
 err = cdev_add (&dev->cdev, devno, 1); 
 //调用cdev_add函数将cdev结构体注册到内核,注册成功后,相应的scull设备就“活”了,其它程序就可以访问scull设备的资源。
 //所以在注册之前,必须确保所有必要的初始化工作都完成了。
 /* 产生错误的处理 */
 if (err)
 	printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
return 0;       /*scull设备创建和初始化成功返回0 */
fail:         /*scull设备创建和初始化失败清除模块 */
 scull_cleanup_module();
 return result;

三个重要数据结构

   大部分的基础性的驱动程序操作涉及到 3 个重要的内核数据结构, 称为 file_operations, file, 和 inode

file_operation结构

   我们已经为自己保留了一些设备编号来使用, 但未将任何程序操作连接到这些编号上。 file_operation 结构是一个字符驱动如何建立这个连接,这个结构, 定义在 <linux/fs.h>, 是一个函数指针的集合。这里用scull的file_operation结构来解析:

struct file_operations scull_fops = { 
  .owner = THIS_MODULE, 
  .llseek = scull_llseek, 
  .read = scull_read, 
  .write = scull_write, 
  .ioctl = scull_ioctl, 
  .open = scull_open, 
  .release = scull_release, 
}; 
  • struct module *owner
    第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针。这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.
  • loff_t (*llseek) (struct file *, loff_t, int);
    llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 要占64 位数据宽度。出错时返回一个负值。如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述)。
  • ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL(“Invalid argument”) 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 “signed size” 类型, 常常是目标平台本地的整数类型)。
  • ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    发送数据给设备。如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数。
  • int (*open) (struct inode *, struct file );
    尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知。
  • int (*release) (struct inode *, struct file *);
    在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL。

file结构

   struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构。文件结构代表一个打开的文件(它不特定给设备驱动; 系统中每个打开的文件有一个关联的 struct file 在内核空间)。
   它由内核在 open 时创建, 并传递给在该文件上操作的任何函数, 直到最后的close函数。在文件的所有实例都关闭后, 内核释放这个数据结构。
   在内核源码中, 指向struct file 的指针常常称为 file 或者 filp(“file pointer”). 我们将一直称这个指针为 filp 以避免和结构自身混淆. 因此, file 指的是结构本身, 而 filp 是结构指针。
   struct file 的重要成员:

  • fmode_t f_mode;
    此文件模式通过 FMODE_READ, FMODE_WRITE 识别了文件为可读的,可写的,或者是二者。在open 或 ioctl 函数中可能需要检查此域以确认文件的读 / 写权限,你不必直接去检测读或写权限,因为在进行 open/ioctl 等操作时内核本身就需要对其权限进行检测。
  • loff_t f_pos;
    当前读写文件的位置,为 64 位。如果想知道当前文件当前位置在哪,驱动可以读取这个值,但不要去修改它的位置; read,write 会使用它们接收到的最后一个指针参数来更新文件的位置,而不是直接对ilp ->f_pos 操作。这一规则的例外llseek 方法, llseek 方法的目的就是用于改变文件的位置。
  • unsigned int f_flags;
    文件标志,如 O_RDONLY, O_NONBLOCK 以及 O_SYNC 。在驱动中还可以检查 O_NONBLOCK 标志查看是否有非阻塞请求。其它的标志较少使用。特别地注意的是,读写权限的检查是使用 f_mode 而不是 f_flog 。所有的标量定义在头文件 <linux/fcntl.h> 中
  • struct file_operations *f_op;
    与文件相关的各种操作。当文件需要迅速进行各种操作时,内核分配这个指针作为它实现文件打开,读,写等功能的一部分。 filp->f_op 其值从未被内核保存作为下次的引用,即你可以改变与文件相关的各种操作,这种方式效率非常高。
  • void *private_data;
    在驱动调用 open 方法之前, open 系统调用设置此指针为 NULL 值。你可以很自由的将其做为你自己需要的一些数据域或者不管它,如,你可以将其指向一个分配好的数据,但是你必须记得在 file struct 被内核销毁之前在 release 方法中释放这些数据的内存空间。 private_data 用于在系统调用期间保存各种状态信息是非常有用的。

inode结构

   内核使用inode结构体在内核内部表示一个文件。因此,它与表示一个已经打开的文件描述符的结构体(即file 文件结构)是不同的,我们可以使用多个file 文件结构表示同一个文件的多个文件描述符,但此时,所有的这些file文件结构全部都必须只能指向一个inode结构体。
   inode结构体包含了一大堆文件相关的信息,但是就针对驱动代码来说,我们只要关心其中的两个域即可:

  • dev_t i_rdev;
    表示设备文件的结点,这个域实际上包含了设备号。
  • struct cdev *i_cdev;
    struct cdev是内核的一个内部结构,它是用来表示字符设备的,当inode结点指向一个字符设备文件时,此域为一个指向inode结构的指针。
  • 此外,内核也提供了两个宏可以从inode结点中获取主次设备号,宏的原型如下:
       unsigned int iminor(struct inode *inode);
       unsigned int imajor(struct inode *inode); 

字符设备程序的操作方法

   scull设备的模块初始化函数在上面实现了。scull驱动程序已经被注册在内核中,该驱动程序不会主动做任何事情,而是等待响应用户程序的访问。我们应该知道,在用户空间,在用户程序看来,设备和其它普通文件一样,都是文件,而操作这些文件的接口就是文件操作函数集,比如open,release,read,write等等。
   用户空间程序通过open,read,write等函数操作设备文件,我们在注册scull设备的cdev时,指定了设备文件操作函数集:
cdev_init(&dev->cdev, &scull_fops);
   而scull_fops就指定了如果用户空间程序执行open,release,read,write操作,应该调用什么函数进行响应。scull_fops定义如下:

/*文件操作结构体*/
struct file_operations scull_fops = {
   .owner =    THIS_MODULE,    
   .llseek =   scull_llseek,      /* seek文件定位函数 */
   .read =     scull_read,      /*读函数*/   .
   .write =    scull_write,      /*写函数*/
   .ioctl =    scull_ioctl,       /*文件控制函数*/
   .open =     scull_open,     /*文件打开函数*/
   .release =  scull_release,     /*文件释放函数*/
};

   通过scull_fops,内核就知道了,如果用户空间程序调用 open操作打开scull相应设备,内核就会执行scull驱动程序的scull_open函数进行响应。其它函数依次类推。如果驱动程序没有定义对应某个用户空间操作的函数,内核就会执行默认动作响应。
   下面就分别来了解这几个函数:

scull_open函数

   在大部分驱动程序中,open函数应该实现:

  • 检查设备特定的错误(例如设备设备未就绪或类似的错误)
  • 如果它第一次打开, 初始化设备
  • 如果需要, 更新 f_op 指针.
  • 分配并填写要放进 filp->privatedata 的数据结构
    源码分析:
int scull_open(struct inode *inode, struct file *filp)
{
	struct scull_dev *dev;    /*获取设备信息 */
	dev = container_of(inode->i_cdev, struct scull_dev, cdev);   //调用container_of宏,通过cdev成员得到包含该cdev的scull_dev结构
	filp->private_data = dev;    /*将得到的scull_dev结构保存在filp->private_data中,因为open结束后,后面的read,write等操作使用同一个filp变量,它们即可以从filp->private_data中直接取出scull_dev结构体来使用。*/
	
	/*如果scull设备文件是以只写的方式打开,则要调用scull_trim将scull设备清空。up(&dev->sem)是进行加锁解锁操作,进行互斥。*/
	if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) {
		if (down_interruptible(&dev->sem))
			return -ERESTARTSYS;
		scull_trim(dev); 
		up(&dev->sem);
	}
	return 0;          /* 打开成功 */
}

   下面介绍scull_trim(dev)清空设备的方法:

int scull_trim(struct scull_dev *dev)
{
	struct scull_qset *next, *dptr;
	int qset = dev->qset;   /* "dev" is not-null */
	int i;
	for (dptr = dev->data; dptr; dptr = next) {   /* dev->data指向第一个量子集scull_qset,所以这个for循环每次循环处理一个scull_qset。 */
		if (dptr->data) {
			for (i = 0; i < qset; i++)        //这个for循环循环1000次,因为每个量子集有1000个量子。
			   kfree(dptr->data[i]);           //每次kfree释放一个量子的内存空间。
			kfree(dptr->data);                //释放量子集数组占用的内存空间。
			dptr->data = NULL;               //将指针重新初始化为NULL。防止野指针。
		}
		next = dptr->next;                    //next指向下一个量子集。
		kfree(dptr);                          //释放scull_qset占用的内存空间。
	}
	/*
	*恢复初始状态。
	*/
	dev->size = 0;
	dev->quantum = scull_quantum;
	dev->qset = scull_qset;
	dev->data = NULL;
	return 0;
}

scull_release函数

   release 方法的角色是 open 的反面,它需要实现的任务很简单:

  • 释放 open 分配在 filp->private_data 中的任何东西
  • 在最后的 close 关闭设备

   因此它的代码也简单:

/*
*这个函数直接返回0。因为scull设备是内存设备,关闭设备时也没有什么需要特别
*理的,所以这个函数比较简单。
*/
int scull_release(struct inode *inode, struct file *filp)
{
	return 0;
}

scull_read函数和scull_write函数

   read和write方法都进行类似的任务, 就是拷贝数据到应用程序空间和反过来到应用程序中拷贝数据. 因此, 它们的原型相当相似:

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); 

   对于 2 个方法:

  • filp 是文件指针;
  • count 是请求的传输数据大小;
  • buff 参数指向用户空间的缓存区, 这个缓冲区要么保存写入的数据,要么是一个存放新读入新数据的空缓冲区。
  • offp是一个指向一个"long offset type"(长偏移量类型)对象指针, 它指出用户在文件中进行存取操作的位置.
    返回值是一个"signed size type"; 有符号的尺寸类型。

   read 方法的任务是从设备拷贝数据到用户空间(使用 copy_to_user), 而 write 方法从用户空间拷贝数据到设备(使用 copy_from_user)。
   下面分别分析scull_read函数和scull_write函数:

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;	      
	int quantum = dev->quantum, qset = dev->qset;
	int itemsize = quantum * qset;     
	int item, s_pos, q_pos, rest;
	ssize_t retval = 0;
	if (down_interruptible(&dev->sem))     //获得互斥锁
		return -ERESTARTSYS;
	if (*f_pos >= dev->size)
		goto out;
	if (*f_pos + count > dev->size)
		count = dev->size - *f_pos;
	item = (long)*f_pos / itemsize;        // item代表要读的数据起始点在哪个scull_qset中
	rest = (long)*f_pos % itemsize;        
	s_pos = rest / quantum; q_pos = rest % quantum;     //s_pos代表要读的数据起始点在哪个量子中,q_pos代表要读的数据的起始点在量子的具体哪个位置
	dptr = scull_follow(dev, item);    /*调用scull_follow函数,这个函数的第二个参数代表要读的数据在哪个scull_qset中,该函数的作用是返回item指定的scull_qset。如果scull_qset不存在,还要分配内存空间,创建指定的scull_qset。*/
	if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])  //如果指定的scull_qset不存在,或者量子指针数组不存在,或者量子不存在,都退出。
		goto out; 

	/* 设置scull_read一次最多只能读一个量子 */
	if (count > quantum - q_pos)
		count = quantum - q_pos;
    /*调用copy_to_user(buf, dptr->data[s_pos] + q_pos, count)函数,将数据拷贝到用户空间。*/
	if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
		retval = -EFAULT;
		goto out;
	} 
	*f_pos += count;    //读取完成后,新的文件指针位置向前移动count个字节
	retval = count;    
  out:
	up(&dev->sem);
	return retval;     //返回读取到的字节数,即count。
}

  read 的返回值的解释:

  • 如果这个值等于传递给 read 系统调用的 count 参数, 请求的字节数已经被传送,
    这是最好的情况.
  • 如果是正数, 但是小于 count, 只有部分数据被传送. 这可能由于几个原因, 依赖
    于设备. 常常, 应用程序重新试着读取. 例如, 如果你使用 fread 函数来读取, 库
    函数重新发出系统调用直到请求的数据传送完成.
  • 如果值为 0, 到达了文件末尾(没有读取数据)。
  • 一个负值表示有一个错误. 这个值指出了什么错误, 根据 <linux/errno.h>。 出错
    的典型返回值包括 -EINTR( 被打断的系统调用) 或者 -EFAULT( 坏地址 )。

  写函数的源代码:

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; 
	if (down_interruptible(&dev->sem))      //获得互斥锁
		return -ERESTARTSYS;
	item = (long)*f_pos / itemsize;         // item代表要写的数据起始点在哪个scull_qset中
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum; q_pos = rest % quantum;   //s_pos代表要写的数据起始点在哪个量子中,q_pos代表要写的数据的起始点在量子的具体哪个位置
 /*调用scull_follow函数,这个函数的第二个参数代表要读的数据在哪个scull_qset中,
	该函数的作用是返回item指定的scull_qset。*/
	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;
	}
	/* 限定一次最多只能写满一个量子。 */
	if (count > quantum - q_pos)
		count = quantum - q_pos;
   /*调用copy_from_user,将用户数据写到量子中。*/
	if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
		retval = -EFAULT;
		goto out;
	}
	*f_pos += count;   //将文件指针后移count字节。
	retval = count;    //设置返回值为count,即写入字节数。

        /* 更新文件大小。 */
	if (dev->size < *f_pos)
		dev->size = *f_pos;

  out:
	up(&dev->sem);
	return retval;
}

  与read类似,write也能传送少于要求的数据, 根据返回值规则:

  • 如果返回值等于 count, 要求的字节数已被传送.
  • 如果返回值是正值, 但是小于 count, 只有部分数据被传送. 程序很可能再次试图写入剩下的数据。
  • 如果值为 0, 意味什么没有写入。 这个结果不是一个错误, 没有理由返回一个错误码; 标准库会重复调用write。
  • 一个负值表示发生一个错误; 和read一样, 有效的错误码是定义在 <linux/errno.h>中。

使用字符设备程序的方法

   前面了解了字符设备程序的一些基本操作方法,但是要使用这些方法的话,还需要给创建出来的设备分配设备节点才行,而创建设备节点又有两种方式:

手动创建设备节点

   手动创建设备节点的话,顾名思义,是在命令行敲命令来给新创建的设备分配设备节点,命令为:
   mkmod 设备名称 主设备号 次设备号
   创建完设备节点后,就可以通过open、read、write方法来对设备进行操作了。

自动创建设备节点

  自动创建设备节点,就是在代码里调用device_creat()或device_register()或device_add()方法来创建设备节点,这三个方法位于<linux/device.h>头文件中,其中最常见的就是用device_create()函数来创建设备节点了,但是在之后阅读内核源码的过程中却很少见device_create()的踪影了,取而代之的是device_register()与device_add(),将device_create()函数展开不难发现:其实device_create()只是device_register()的封装,而device_register()则是device_add()的封装。
  以一个简单的led设备字符设备驱动为例,下面分别用device_create()、device_register()、device_add()三个函数来创建设备节点“/dev/led”:

device_create()方法

static class *led_class;
static int __init led_init(void)
{
  int ret;
  dev_t devno;
  struct cdev *cdev;
  struct dev *dev;
  /* 注册设备号 */
  ret = alloc_chrdev_region(&devno, 0, 1, "led");
  if (ret < 0) 
      return ret;
  /* 分配、初始化、注册cdev*/
  cdev = cdev_alloc();
  if (IS_ERR(cdev)) {
      ret = PTR_ERR(cdev);
      goto out_unregister_devno;
  }
  cdev_init(&cdev, &led_fops);
  cdev.owner = THIS_MODULE;
  ret = cdev_add(&cdev, devno, 1);    
  if (ret) 
      goto out_free_cdev;
  /* 创建设备类 */
  led_class = class_create(THIS_MODULE, "led_class");
  if (IS_ERR(led_class)) {
      ret = PTR_ERR(led_class);
      goto out_unregister_cdev;
  } 

  /* 创建设备节点 */
  dev = device_create(led_class, NULL, devno, NULL, "led");
  if (IS_ERR(dev)) {
      ret = PTR_ERR(dev);
      goto out_del_class;
  }
  return 0;
out_del_class:
  class_destroy(led_class); 
out_unregister_cdev:
  cdev_del(cdev);
out_free_cdev:
  kfree(cdev);
out_unregister_devno:
  unregister_chrdev_region(devno, 1);
  return ret;
}
module_init(led_init);

  device_register()方法

static class *led_class;
static int __init led_init(void)
{
    ......
     /* 注册设备号 */
    ......
    /* 分配、初始化、注册cdev*/
    ......
    /* 创建设备类 */
    ......
   /*分配内存*/
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        ret = -ENOMEM;
        goto out_del_class;
    }
 /* 创建设备节点 */
    dev->class = led_class;         // 关联设备类
    dev->parent = NULL;             
    dev->devt = devno;              // 关联设备号
    dev_set_drvdata(dev, NULL);     
    dev_set_name(dev, "led");       // 设置节点名字
    dev->release = device_create_release;

    ret = device_register(dev);
    if (ret) 
        goto out_put_dev;
    return 0;
out_put_dev:
    put_device(dev);
    kree(dev);
out_del_class:
    class_destroy(led_class);    
out_unregister_cdev:    
    cdev_del(cdev);
out_free_cdev:
    kfree(cdev);
out_unregister_devno:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(led_init);

  device_add()方法

static class *led_class;
static int __init led_init(void)
{
    ......
     /* 注册设备号 */
    ......
    /* 分配、初始化、注册cdev*/
    ......
    /* 创建设备类 */
    ......
  /*分配内存*/
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        ret = -ENOMEM;
        goto out_del_class;
    }
  /* 创建设备节点 */
    dev->class = led_class;         // 关联设备类
    dev->parent = NULL;             
    dev->devt = devno;              // 关联设备号
    dev_set_drvdata(dev, NULL);     
    dev_set_name(dev, "led");       // 设置节点名字
    dev->release = device_create_release;
    device_initialize(dev);
    ret = device_add(dev);
    if (ret) 
        goto out_put_dev;
    return 0;
out_put_dev:
    put_device(dev);
    kree(dev);
out_del_class:
    class_destroy(led_class);    
out_unregister_cdev:    
    cdev_del(cdev);
out_free_cdev:
    kfree(cdev);
out_unregister_devno:
    unregister_chrdev_region(devno, 1);
    return ret;
}
module_init(led_init);

  注意:直接使用源码并不能运行出结果,因为在后续的一些内核版本中修改了一部分的参数以及宏的定义,源码分析的作用主要用来理解字符驱动的实现,如果想要调通源码的话,可以自行百度修改,但建议自己动手实现,因为字符驱动还是比较简单的,还有在用户空间调用驱动程序的话,要记得创建和分配设备节点,不然无法调用对应驱动程序的方法。
  如果想尝试比较简单的字符驱动实现,可以查看我的另一篇简单字符设备驱动实现的文章:https://blog.csdn.net/baidu_38661691/article/details/94601963

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值