一文带你掌握Linux字符设备架构

1、Linux设备分类

Linux系统为了管理方便,将设备分红三种基本类型:node

  • 字符设备
  • 块设备
  • 网络设备

字符设备:

字符(char)设备是个可以像字节流(相似文件)同样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序一般至少要实现open、close、read和write的系统调用。linux

字符终端(/dev/console)和串口(/dev/ttyS0以及相似设备)就是两个字符设备,它们能很好的说明“流”这种抽象概念。程序员

字符设备能够经过文件节点来访问,好比/dev/tty1和/dev/lp0等。这些设备文件和普通文件之间的惟一差异在于对普通文件的访问能够先后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。然而,也存在具备数据区特性的字符设备,访问它们时可先后移动访问位置。例如framebuffer就是这样的一个设备,app能够用mmap或lseek访问抓取的整个图像。web

在/dev下执行ls -l ,能够看到不少建立好的设备节点:shell

字符设备文件(类型为c),设备文件是没有文件大小的,取而代之的是两个号码:主设备号5 +次设备号1 。微信

块设备:

和字符设备相似,块设备也是经过/dev目录下的文件系统节点来访问。块设备(例如磁盘)上可以容纳filesystem。在大多数的Unix系统中,进行I/O操做时块设备每次只能传输一个或多个完整的块,而每块包含512字节(或2的更高次幂字节的数据)。网络

Linux可让app像字符设备同样地读写块设备,容许一次传递任意多字节的数据。所以,块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不一样对用户来说是透明的。在内核中,和字符驱动程序相比,块驱动程序具备彻底不一样的接口。多线程

块设备文件(类型为b):架构

网络设备:

任何网络事物都须要通过一个网络接口造成,网络接口是一个可以和其余主机交换数据的设备。接口一般是一个硬件设备,但也多是个纯软件设备,好比回环(loopback)接口。app

网络接口由内核中的网络子系统驱动,负责发送和接收数据包。许多网络链接(尤为是使用TCP协议的链接)是面向流的,但网络设备却围绕数据包的传送和接收而设计。网络驱动程序不须要知道各个链接的相关信息,它只要处理数据包便可。

因为不是面向流的设备,所以将网络接口映射到filesystem中的节点(好比/dev/tty1)比较困难。

Unix访问网络接口的方法仍然是给它们分配一个惟一的名字(好比eth0),但这个名字在filesystem中不存在对应的节点。内核和网络设备驱动程序间的通讯,彻底不一样于内核和字符以及块驱动程序之间的通讯,内核调用一套和数据包相关的函数socket,也叫套接字。

查看网络设备使用命令ifconfig:

2、字符设备架构是如何实现的?

在Linux的世界里面一切皆文件,全部的硬件设备操做到应用层都会被抽象成文件的操做。咱们知道若是应用层要访问硬件设备,它一定要调用到硬件对应的驱动程序。Linux内核中有那么多驱动程序,应用层怎么才能精确的调用到底层的驱动程序呢?

在这里咱们字符设备为例,来看一下应用程序是如何和底层驱动程序关联起来的。必须知道的基础知识:

  • 1.在Linux文件系统中,每一个文件都用一个struct inode结构体来描述,这个结构体里面记录了这个文件的全部信息,例如:文件类型,访问权限等。

  • 2.在Linux操做系统中,每一个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,而且该文件会有对应的主设备号和次设备号。

  • 3.在Linux操做系统中,每一个驱动程序都要分配一个主设备号,字符设备的设备号保存在struct cdev结构体中。

 struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;//接口函数集合
        struct list_head list;//内核链表
        dev_t dev;    //设备号
        unsigned int count;//次设备号个数
    };
  • 4.在Linux操做系统中,每打开一次文件,Linux操做系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。

注意:

经常咱们认为struct inode描述的是文件的静态信息,即这些信息不多会改变。而struct file描述的是动态信息,即在对文件的操做的时候,struct file里面的信息常常会发生变化。典型的是struct file结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。

这几个结构体关系以下图所示:

经过上图咱们能够知道,若是想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程当中,Linux内核将应用层和对应的驱动程序关联起来。

  • 1.当open函数打开设备文件时,能够根据设备文件对应的struct inode结构体描述的信息,能够知道接下来要操做的设备类型(字符设备仍是块设备)。还会分配一个struct file结构体。

  • 2.根据struct inode结构体里面记录的设备号,能够找到对应的驱动程序。这里以字符设备为例。在Linux操做系统中每一个字符设备有一个struct cdev结构体。此结构体描述了字符设备全部的信息,其中最重要一项的就是字符设备的操做函数接口。

  • 3.找到struct cdev结构体后,Linux内核就会将struct cdev结构体所在的内存空间首地记录在struct inode结构体的i_cdev成员中。将struct cdev结构体的中记录的函数操做接口地址记录在struct file结构体的f_op成员中。

  • 4.任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层的应用程序就能够经过fd来找到strut file,而后在由struct file找到操做字符设备的函数接口了。

3、字符驱动相关函数分析

/**
 * cdev_init() - initialize a cdev structure
 * @cdev: the structure to initialize
 * @fops: the file_operations for this device
 *
 * Initializes @cdev, remembering @fops, making it ready to add to the
 * system with cdev_add().
 */

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
功能:
  初始化cdev结构体
参数:
  @cdev cdev结构体地址
  @fops 操做字符设备的函数接口地址
返回值:
  无
/**
 * register_chrdev_region() - register a range of device numbers
 * @from: the first in the desired range of device numbers; must include
 *        the major number.
 * @count: the number of consecutive device numbers required
 * @name: the name of the device or driver.
 *
 * Return value is zero on success, a negative error code on failure.
 */
                                              
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:
  注册一个范围()的设备号
参数:
  @from 设备号
  @count 注册的设备个数
  @name 设备的名字
返回值:
  成功返回0,失败返回错误码(负数)
/**
 * 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)
功能:
  添加一个字符设备到操做系统
参数:
  @p cdev结构体地址
  @dev 设备号
  @count 次设备号个数
返回值:
  成功返回0,失败返回错误码(负数)
/**
 * cdev_del() - remove a cdev from the system
 * @p: the cdev structure to be removed
 *
 * cdev_del() removes @p from the system, possibly freeing the structure
 * itself.
 */

void cdev_del(struct cdev *p)
功能:
  从系统中删除一个字符设备
参数:
  @p cdev结构体地址
返回值:
  无
static inline int register_chrdev(unsigned int major, const char *name,
          const struct file_operations *fops)


功能:
  注册或者分配设备号,并注册fops到cdev结构体,
  若是major>0,功能为注册该主设备号,
  若是major
=0,功能为动态分配主设备号。
参数:
  @major : 主设备号
  @name : 设备名称,执行 cat /proc/devices显示的名称
  @fops  : 文件系统的接口指针
返回值
  若是major>0   成功返回0,失败返回负的错误码
  若是major=0  成功返回主设备号,失败返回负的错误码

该函数实现了对cdev的初始化和注册的封装,因此调用该函数以后就不须要本身操做cdev了。

相对的注销函数为unregister_chrdev

static inline void unregister_chrdev(unsigned int major, const char *name)

4、如何编写字符设备驱动

参考上图,编写字符设备驱动步骤以下:

1. 实现模块加载和卸载入口函数

module_init (hello_init);
module_exit (hello_exit);

2. 申请主设备号

申请主设备号  (内核中用于区分和管理不一样字符设备)

register_chrdev_region (devno, number_of_devices, "hello");

3. 建立设备节点

建立设备节点文件 (为用户提供一个可操做到文件接口--open()) 建立设备节点有两种方式:手动方式建立,函数自动建立。手动建立:

mknod /dev/hello c 250 0

自动建立设备节点

除了使用mknod命令手动建立设备节点,还能够利用linux的udev、mdev机制,而咱们的ARM开发板上移植的busybox有mdev机制,那么就使用mdev机制来自动建立设备节点。

在etc/init.d/rcS文件里有一句:

echo /sbin/mdev > /proc/sys/kernel/hotplug

该名命令就是用来自动建立设备节点。

udev 是一个工做在用户空间的工具,它能根据系统中硬件设备的状态动态的更新设备文件,包括设备文件的建立,删除,权限等。这些文件一般都定义在/dev 目录下,但也能够在配置文件中指定。udev 必须有内核中的sysfs和tmpfs支持,sysfs 为udev 提供设备入口和uevent 通道,tmpfs 为udev 设备文件提供存放空间。

udev 运行在用户模式,而非内核中。udev 的初始化脚本在系统启动时建立设备节点,而且当插入新设备——加入驱动模块——在sysfs上注册新的数据后,udev会创新新的设备节点。

注意,udev 是经过对内核产生的设备文件修改,或增长别名的方式来达到自定义设备文件的目的。可是,udev 是用户模式程序,其不会更改内核行为。也就是说,内核仍然会建立sda,sdb等设备文件,而udev可根据设备的惟一信息来区分不一样的设备,并产生新的设备文件(或连接)。

例如:

若是驱动模块能够将本身的设备号做为内核参数导出,在sysfs文件中就有一个叫作uevent文件记录它的值。

由上图可知,uevent中包含了主设备号和次设备号的值以及设备名字。

在Linux应用层启动一个udev程序,这个程序的第一次运行的时候,会遍历/sys目录,寻找每一个子目录的uevent文件,从这些uevent文件中获取建立设备节点的信息,而后调用mknod程序在/dev目录下建立设备节点。结束以后,udev就开始等待内核空间的event。这个设备模型的东西,咱们在后面再详细说。这里大就能够这样理解,在Linux内核中提供了一些函数接口,经过这些函数接口,咱们可在sysfs文件系统中导出咱们的设备号的值,导出值以后,内核还会向应用层上报event。此时udev就知道有活能够干了,它收到这个event后,就读取event对应的信息,接下来就开始建立设备节点啦。

如何建立一个设备类?

第一步 :经过宏class_create() 建立一个class类型的对象;

/* This is a #define to keep the compiler from merging different
 * instances of the __key variable */

#define class_create(owner, name)    \
({            \
  static struct lock_class_key __key;  \
  __class_create(owner, name, &__key);  \
})


参数:
  @owner  THIS_MODULE
  @name   类名字
返回值
  能够定义一个struct class的指针变量cls接受返回值,而后经过IS_ERR(cls)判断
  是否失败,若是成功这个宏返回0,失败返回非9值(能够经过PTR_ERR(cls)来得到
  失败返回的错误码)

在Linux内核中,把设备进行了分类,同一类设备能够放在同一个目录下,该函数启示就是建立了一个类,例如:

第二步:导出咱们的设备信息到用户空间

/**
 * device_create - creates a device and registers it with sysfs
 * @class: pointer to the struct class that this device should be registered to
 * @parent: pointer to the parent struct device of this new device, if any
 * @devt: the dev_t for the char device to be added
 * @drvdata: the data to be added to the device for callbacks
 * @fmt: string for the device's name
 *
 * This function can be used by char device classes.  A struct device
 * will be created in sysfs, registered to the specified class.
 *
 * A "dev" file will be created, showing the dev_t for the device, if
 * the dev_t is not 0,0.
 * If a pointer to a parent struct device is passed in, the newly created
 * struct device will be a child of that device in sysfs.
 * The pointer to the struct device will be returned from the call.
 * Any further sysfs files that might be required can be created using this
 * pointer.
 *
 * Returns &struct device pointer on success, or ERR_PTR() on error.
 *
 * Note: the struct class passed to this function must have previously
 * been created with a call to class_create().
 */

struct device *device_create(struct class *class, struct device *parent,
           dev_t devt, void *drvdata, const char *fmt, ...)

自动建立设备节点使用实例:

static struct class *cls;
static struct device *test_device;

  devno = MKDEV(major,minor);
  cls = class_create(THIS_MODULE,"helloclass");
  if(IS_ERR(cls))
  {
    unregister_chrdev(major,"hello");
    return result;
  }
  test_device = device_create(cls,NULL,devno,NULL,"hellodevice");
  if(IS_ERR(test_device ))
  {
    class_destroy(cls);
    unregister_chrdev(major,"hello");
    return result;
  }

4 实现file_operations

static const struct file_operations fifo_operations = {
    .owner =   THIS_MODULE,
    .open =   dev_fifo_open,
    .read =   dev_fifo_read,
    .write =   dev_fifo_write,
    .unlocked_ioctl =   dev_fifo_unlocked_ioctl,
};

open、release对应应用层的open()、close()函数。实现比较简单,

直接返回0便可。 其中read、write、unloched_ioctrl 函数的实现须要涉及到用户空间 和内存空间的数据拷贝。

在Linux操做系统中,用户空间和内核空间是相互独立的。也就是说内核空间是不能直接访问用户空间内存地址,同理用户空间也不能直接访问内核空间内存地址。

若是想实现,将用户空间的数据拷贝到内核空间或将内核空间数据拷贝到用户空间,就必须借助内核给咱们提供的接口来完成。

1. read接口实现

用户空间-->内核空间

字符设备的write接口定义以下:

ssize_t (*write)(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
参数:
  filp:待操做的设备文件file结构体指针
  buf:待写入所读取数据的用户空间缓冲区指针
  count:待读取数据字节数
  f_pos:待读取数据文件位置,写入完成后根据实际写入字节数从新定位
返回:
  成功实际写入的字节数,失败返回负值

若是该操做为空,将使得write系统调用返回负EINVAL失败,正常返回实际写入的字节数。

用户空间向内核空间拷贝数据须要使用copy_from_user函数,该函数定义在arch/arm/include/asm/uaccess.h中。

static inline int copy_from_user(void *to, const void __user volatile *from,unsigned long n)
参数:
  to:目标地址(内核空间)
  from:源地址(用户空间)
  n:将要拷贝数据的字节数
返回:
  成功返回0,失败返回没有拷贝成功的数据字节数

还可使用get_user宏:

int get_user(data, ptr);
参数:
  data:能够是字节、半字、字、双字类型的内核变量
  ptr:用户空间内存指针
返回:
  成功返回0,失败返回非0

2. write接口实现

内核空间-->用户空间

字符设备的read接口定义以下:

ssize_t (*read)(struct file *filp, char __user *buf, size_t  count, lofft *f_pos);
参数:
  filp: 待操做的设备文件file结构体指针
  buf:  待写入所读取数据的用户空间缓冲区指针
  count:待读取数据字节数
  f_pos:待读取数据文件位置,读取完成后根据实际读取字节数从新定位
  __user :是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址。

返回值:
  成功实际读取的字节数,失败返回负值

注意:若是该操做为空,将使得read系统调用返回负EINVAL失败,正常返回实际读取的字节数。

用户空间从内核空间读取数据须要使用copy_to_user函数:

 static inline int copy_to_user(void __user volatile *to, const void *from,unsigned long n)
参数:
  to:目标地址(用户空间)
  from:源地址(内核空间)
  n:将要拷贝数据的字节数
返回:
  成功返回0,失败返回没有拷贝成功的数据字节数
在这里插入图片描述

还可使用put_user宏:

int put_user(data, prt)
参数:
  data:能够是字节、半字、字、双字类型的内核变量
  ptr:用户空间内存指针
返回:
  成功返回0, 失败返回非0

这样咱们就能够实现read、write函数了,实例以下:

ssize_t hello_read (struct file *filp, char *buff,   size_t count, loff_t *offp)
{
  ssize_t   result = 0;

  if (count   > 127
    count = 127;

  if   (copy_to_user (buff, data, count))
  {
    result =   -EFAULT;
  }
  else
  {
    printk   (KERN_INFO "wrote %d bytes\n", count);
    result =   count;
  } 
  return   result;
}
ssize_t hello_write (struct file *filp,const char *buf, size_t count, loff_t *f_pos)
{
  ssize_t ret   = 0;
  //printk   (KERN_INFO "Writing %d bytes\n", count);
  if (count   > 127return -ENOMEM;

  if   (copy_from_user (data, buf, count)) {
    ret =   -EFAULT;
  }
  else {
    data[count] = '\0';
    printk   (KERN_INFO"Received: %s\n", data);
    ret =   count;
  }
  return ret;
}

3. unlocked_ioctl接口实现

(1)为何要实现xxx_ioctl ?

前面咱们在驱动中已经实现了读写接口,经过这些接口咱们能够完成对设备的读写。可是不少时候咱们的应用层工程师除了要对设备进行读写数据以外,还但愿能够对设备进行控制。例如:针对串口设备,驱动层除了须要提供对串口的读写以外,还需提供对串口波特率、奇偶校验位、终止位的设置,这些配置信息须要从应用层传递一些基本数据,仅仅是数据类型不一样。

经过xxx_ioctl函数接口,能够提供对设备的控制能力,增长驱动程序的灵活性。

(2)如何实现xxx_ioctl函数接口?

增长xxx_ioctl函数接口,应用层能够经过ioctl系统调用,根据不一样的命令来操做dev_fifo。

kernel 2.6.35 及以前的版本中struct file_operations 一共有3个ioctl :ioctl,unlocked_ioctl和compat_ioctl 如今只有unlocked_ioctl和compat_ioctl 了

在kernel 2.6.36 中已经彻底删除了struct file_operations 中的ioctl 函数指针,取而代之的是unlocked_ioctl 。

·         2.6.36 以前的内核

long (ioctl) (struct inode node ,struct file* filp, unsigned int cmd,unsigned long arg)

·         2.6.36以后的内核

long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)

参数cmd: 经过应用函数ioctl传递下来的命令

先来看看应用层的ioctl和驱动层的xxx_ioctl对应关系:<1>应用层ioctl参数分析

int ioctl(int fd, int cmd, ...);
参数:
@fd:打开设备文件的时候得到文件描述符 
@ cmd:第二个参数:给驱动层传递的命令,须要注意的时候,驱动层的命令和应用层的命令必定要统一
@第三个参数: "..."在C语言中,不少时候都被理解成可变参数。
返回值
       成功:0
       失败:-1,同时设置errno

小贴士:

当咱们经过ioctl调用驱动层xxx_ioctl的时候,有三种状况可供选择:

1: 不传递数据给xxx_ioctl 
2: 传递数据给xxx_ioctl,但愿它最终能把数据写入设备(例如:设置串口的波特率)
3: 调用xxxx_ioctl但愿获取设备的硬件参数(例如:获取当前串口设备的波特率)
这三种状况中,有些时候须要传递数据,有些时候不须要传递数据。在C语言中,是
没法实现函数重载的。那怎么办?用"..."来欺骗编译器了,"..."原本的意思是传
递多参数。在这里的意思是带一个参数仍是不带参数。

参数能够传递整型值,也能够传递某块内存的地址,内核接口函数必须根据实际状况
提取对应的信息。

<2>驱动层xxx_ioctl参数分析

long (*unlocked_ioctl) (struct file *file, unsigned int cmd, unsigned long arg);
参数:
@file:   vfs层为打开字符设备文件的进程建立的结构体,用于存放文件的动态信息 
@ cmd: 用户空间传递的命令,能够根据不一样的命令作不一样的事情
@第三个参数: 用户空间的数据,主要这个数据多是一个地址值(用户空间传递的是一个地址),也多是一个数值,也可能没值
返回值
       成功:0
       失败:带错误码的负值

<3>如何肯定cmd 的值。

该值主要用于区分命令的类型,虽然我只须要传递任意一个整型值便可,可是咱们尽可能按照内核规范要求,充分利用这32bite的空间,若是你们都没有规矩,又如何能成方圆?

如今我就来看看,在Linux 内核中这个cmd是如何设计的吧!

具体含义以下:

设备类型 类型或叫幻数,表明一类设备,通常用一个字母或者1个8bit的数字
序列号 表明这个设备的第几个命令
方 向 表示是由内核空间到用户空间,或是用户空间到内核空间,入:只读,只写,读写,其余
数据尺寸 表示须要读写的参数大小

由上能够一个命令由4个部分组成,每一个部分须要的bite都不彻底同样,制做一个命令须要在不一样的位域写不一样的数字,Linux 系统已经给咱们封装好了宏,咱们只须要直接调用宏来设计命令便可。

在这里插入图片描述

经过Linux 系统给咱们提供的宏,咱们在设计命令的时候,只须要指定设备类型、命令序号,数据类型三个字段就能够了。

Linux 系统中已经设计了一场用的命令,能够经过查阅Linux 源码中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已经被使用过了。

<4> 如何检查命令?

能够经过宏_IOC_TYPE(nr)来判断应用程序传下来的命令type是否正确;

能够经过宏_IOC_DIR(nr)来获得命令是读仍是写,而后再经过宏access_ok(type,addr,size)来判断用户层传递的内存地址是否合法。

使用方法以下:

  if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
    pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
    return-ENOTTY;
  }
  if(_IOC_DIR(cmd)&_IOC_READ)
    ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
  else if( _IOC_DIR(cmd)&_IOC_WRITE )
    ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
  if(ret){
    pr_err("bad   access %ld.\n",ret);
    return-EFAULT;
  }

5 注册cdev

定义好file_operations结构体,就能够经过函数cdev_init()、cdev_add()注册字符设备驱动了。

实例以下:

static struct cdev cdev;

cdev_init(&cdev,&hello_ops);
error = cdev_add(&cdev,devno,1);

注意若是使用了函数register_chrdev(),就不用了执行上述操做,由于该函数已经实现了对cdev的封装。

5、实例

千言万语,所有汇总在这一个图里,你们能够对照相应的层次来学习。

6、实例

好了,如今咱们能够来实现一个完整的字符设备框架的实例,包括打开、关闭、读写、ioctrl、自动建立设备节点等功能。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
#include "dev_fifo_head.h"

//指定的主设备号
#define   MAJOR_NUM 250

//本身的字符设备
struct mycdev
{

    int len;
    unsigned   char buffer[50];
    struct   cdev cdev;
};

MODULE_LICENSE("GPL");
//设备号
static dev_t   dev_num = {0};
//全局gcd
struct mycdev *gcd;
//设备类
struct class *cls;
//得到用户传递的数据,根据它来决定注册的设备个数
static int ndevices = 1;
module_param(ndevices, int0644);
MODULE_PARM_DESC(ndevices, "The number of devices for register.\n");

//打开设备
static int dev_fifo_open(struct   inode *inode,   struct file *file)
{
    struct   mycdev *cd;  

    printk("dev_fifo_open   success!\n");  
    //用struct file的文件私有数据指针保存struct mycdev结构体指针
    cd   = container_of(inode->i_cdev,struct   mycdev,cdev);
    file->private_data =   cd;  
    return   0;
}

//读设备
static ssize_t   dev_fifo_read(struct file *file, char   __user *ubuf,   size_t
size, loff_t *ppos)

{
    int n;
    int ret;
    char   *kbuf;
    struct   mycdev *mycd =   file->private_data;

    printk("read *ppos :   %lld\n",*ppos); 

    if(*ppos == mycd->len)
        return   0;

    //请求大大小 > buffer剩余的字节数   :读取实际记得字节数
    if(size > mycd->len - *ppos)
        n = mycd->len - *ppos;
    else
        n = size;

    printk("n =   %d\n",n);
    //从上一次文件位置指针的位置开始读取数据
    kbuf   = mycd->buffer   + *ppos;
    //拷贝数据到用户空间
    ret   = copy_to_user(ubuf,kbuf, n);
    if(ret != 0)
        return   -EFAULT;

    //更新文件位置指针的值
    *ppos += n;
    printk("dev_fifo_read   success!\n");
    return   n;
}
//写设备
static ssize_t   dev_fifo_write(struct file *file, const char __user *ubuf,size_t size, loff_t *ppos)
{
    int n;
    int ret;
    char   *kbuf;
    struct   mycdev *mycd =   file->private_data;

    printk("write *ppos :   %lld\n",*ppos);
    //已经到达buffer尾部了
    if(*ppos == sizeof(mycd->buffer))
       return   -1;
    //请求大大小 > buffer剩余的字节数(有多少空间就写多少数据)
    if(size > sizeof(mycd->buffer) - *ppos)
        n = sizeof(mycd->buffer) - *ppos;
    else
        n = size;
    //从上一次文件位置指针的位置开始写入数据

    kbuf   = mycd->buffer   + *ppos;
    //拷贝数据到内核空间
    ret   = copy_from_user(kbuf, ubuf, n);
    if(ret != 0)
        return   -EFAULT;

    //更新文件位置指针的值
    *ppos += n;
    //更新dev_fifo.len
    mycd->len += n;
    printk("dev_fifo_write   success!\n");
    return   n;
}

//linux 内核在2.6之后,已经废弃了ioctl函数指针结构,取而代之的是

long   dev_fifo_unlocked_ioctl(struct file *file,   unsigned int cmd,
    unsigned   long arg)

{
  int ret = 0;
  struct mycdev *mycd   = file->private_data;

  if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
    pr_err("cmd   %u,bad magic 0x%x/0x%x.\n",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
    return-ENOTTY;
  }
  if(_IOC_DIR(cmd)&_IOC_READ)
    ret=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
  else if( _IOC_DIR(cmd)&_IOC_WRITE )
    ret=!access_ok(VERIFY_READ,(void   __user*)arg,_IOC_SIZE(cmd));
  if(ret){
    pr_err("bad   access %ld.\n",ret);
    return-EFAULT;
  } 
    switch(cmd)
    {
      case DEV_FIFO_CLEAN:
         printk("CMD:CLEAN\n");
      memset(mycd->buffer, 0sizeof(mycd->buffer));
         break;
      case DEV_FIFO_SETVALUE:
         printk("CMD:SETVALUE\n");
         mycd->len = arg;
         break;
      case DEV_FIFO_GETVALUE:
         printk("CMD:GETVALUE\n");
         ret   = put_user(mycd->len, (int *)arg);
         break;
      default:
         return   -EFAULT;
    }
    return   ret;
}

//设备操做函数接口

static const struct file_operations fifo_operations = {
    .owner =   THIS_MODULE,
    .open =   dev_fifo_open,
    .read =   dev_fifo_read,
    .write =   dev_fifo_write,
    .unlocked_ioctl =   dev_fifo_unlocked_ioctl,
};
//模块入口
int __init dev_fifo_init(void)
{
    int i = 0;
    int n = 0;
    int ret;

    struct   device *device;
  gcd   = kzalloc(ndevices   * sizeof(struct   mycdev), GFP_KERNEL);

    if(!gcd){
        return   -ENOMEM;
    }

    //设备号 : 主设备号(12bit) | 次设备号(20bit)
    dev_num   = MKDEV(MAJOR_NUM, 0);
    //静态注册设备号
    ret   = register_chrdev_region(dev_num,ndevices,"dev_fifo");
    if(ret < 0){
    //静态注册失败,进行动态注册设备号
     ret   =alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
      if(ret < 0){
        printk("Fail to register_chrdev_region\n");
        goto   err_register_chrdev_region;
      }
    }
    //建立设备类
    cls   = class_create(THIS_MODULE, "dev_fifo");
    if(IS_ERR(cls)){
        ret   = PTR_ERR(cls);
        goto   err_class_create;
    }
    printk("ndevices :   %d\n",ndevices);
    for(n = 0;n < ndevices;n   ++)
    {
      //初始化字符设备
      cdev_init(&gcd[n].cdev,&fifo_operations);
      //添加设备到操做系统
      ret   = cdev_add(&gcd[n].cdev,dev_num + n,1);
      if (ret < 0)
      {
         goto   err_cdev_add;
      }
     //导出设备信息到用户空间(/sys/class/类名/设备名)
      device   = device_create(cls,NULL,dev_num +n,NULL,"dev_fifo%d",n);
      if(IS_ERR(device)){
         ret   = PTR_ERR(device);
         printk("Fail to device_create\n");
         goto   err_device_create;    
      }
    }
    printk("Register   dev_fito to system,ok!\n");
    return   0;
err_device_create:

    //将已经导出的设备信息除去
    for(i = 0;i < n;i ++)
    {
       device_destroy(cls,dev_num + i);    
    }
err_cdev_add:
    //将已经添加的所有除去
    for(i = 0;i < n;i ++)
    {
       cdev_del(&gcd[i].cdev);
    }
err_class_create:
    unregister_chrdev_region(dev_num,   ndevices);
err_register_chrdev_region:
    return   ret;
}
void __exit dev_fifo_exit(void)
{
    int i;
    //删除sysfs文件系统中的设备
    for(i = 0;i < ndevices;i   ++)
    {
        device_destroy(cls,dev_num + i);    
    }
    //删除系统中的设备类
    class_destroy(cls);
    //从系统中删除添加的字符设备
    for(i = 0;i < ndevices;i   ++)
    {
       cdev_del(&gcd[i].cdev);
    } 
    //释放申请的设备号
    unregister_chrdev_region(dev_num,   ndevices);
    return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);   

头文件内容:

dev_fifo_head.h

#ifndef _DEV_FIFO_HEAD_H
#define _DEV_FIFO_HEAD_H
#define DEV_FIFO_TYPE 'k'
#define DEV_FIFO_CLEAN _IO(DEV_FIFO_TYPE,0x10)
#define DEV_FIFO_GETVALUE _IOR(DEV_FIFO_TYPE,0x11,int)
#define DEV_FIFO_SETVALUE _IOW(DEV_FIFO_TYPE,0x12,int)
#endif

Makefile :

ifeq ($(KERNELRELEASE),)
KERNEL_DIR ?=/lib/modules/$(shell uname -r)/build  
PWD :=$(shell pwd)
modules:
    $(MAKE) -C $(KERNEL_DIR)   M=$(PWD) modules
.PHONY:modules clean
clean:
    $(MAKE) -C $(KERNEL_DIR)   M=$(PWD) clean
else
    obj-m := dev_fifo.o  
endif

应用程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, const char *argv[])
{
    int fd ;
    int n;
    char buf[1024] = "hello   word";
    
    fd = open("/dev/dev_fifo0",O_RDWR);
    if(fd < 0){
        perror("Fail   ot open");
        return   -1;
    }
    printf("open   successful ,fd = %d\n",fd);
    n = write(fd,buf,strlen(buf));
    if(n < 0){
        perror("Fail   to write");
        return   -1;
    }
    printf("write   %d bytes!\n",n);
    n = write(fd,buf,strlen(buf));
    if(n < 0){
        perror("Fail   to write");
        return   -1;
    }
    printf("write   %d bytes!\n",n);
    return 0;
}

测试步骤:

(1)   加载模块

sudo insmod hello.ko

(2)   建立设备节点

sudo mknod /dev/hello c 250 0

若是代码中增长了自动建立设备节点的功能,这个步骤不要执行。

(3)   测试字符设备

gcc test.c -o run
sudo ./run
 


 


其余网友提问汇总


 1. 两个线程,两个互斥锁,怎么造成一个死循环?


 2. 一个端口号能够同时被两个进程绑定吗?


 3. 一个多线程的简单例子让你看清线程调度的随机性

4. 粉丝提问|c语言:如何定义一个和库函数名同样的函数,并在函数中调用该库函数

5.  [网友问答5]i2c的设备树和驱动是如何匹配以及什么时候调用probe的?

6. [粉丝问答6]子进程进程的父进程关系

7. 【粉丝问答7】局域网内终端是如何访问外网?答案在最后




推荐阅读


【1】嵌入式工程师到底要不要学习ARM汇编指令?必读
【2】 Modbus协议概念最详细介绍 必读
【3】嵌入式工程师到底要不要学习ARM汇编指令?
【4】【从0学ARM】你不了解的ARM处理异常之道
【5】 4. 从0开始学ARM-ARM汇编指令其实很简单
【6】 为何使用结构体效率比较高? 必读

 

进群,请加一口君我的微信,带你嵌入式入门进阶。

 


本文分享自微信公众号 - 一口Linux(yikoulinux)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索