Linux字符设备驱动

转载自:https://www.cnblogs.com/chen-farsight/p/6155518.html

https://www.cnblogs.com/geneil/archive/2011/12/04/2275372.html

https://blog.csdn.net/fh400/article/details/6929115

https://blog.csdn.net/zqixiao_09/article/details/50859302

https://www.cnblogs.com/chen-farsight/p/6177870.html

http://www.manongjc.com/article/54517.html

一、字符设备基础

字符设备:是指只能一个字节一个字节进行读写操作的设备,不能随机读取设备中的某一数据、读取数据要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等。

一般每个字符设备或者块设备都会在/dev目录(可以是任意目录,这样是为了统一)下对应一个设备文件。linux用户层程序通过设备文件来使用驱动程序操作字符设备或块设备。

二、字符设备驱动与用户空间访问该设备的程序三者之间的关系

字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备、其驱动程序中完成的主要工作是初始化、添加和删除 struct cdev 结构体,申请和释放设备号,以及填充 struct file_operations 结构体中断的操作函数,实现 struct file_operations 结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。

如图,在Linux内核代码中:

  • 使用struct cdev结构体来抽象一个字符设备;
  • 通过一个dev_t类型的设备号(分为主(major)、次设备号(minor))一确定字符设备唯一性;
  • 通过struct file_operations类型的操作方法集来定义字符设备提供个VFS的接口函数。 

  三、字符设备模型

 

1、设备号申请/释放

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

linux内核中,设备号用dev_t来描述:

typedef u_long dev_t;  // 在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。 

内核也为我们提供了几个方便的宏操作dev_t: 包括获取主、次设备号,将主次设备号拼凑为设备号。

#define MAJOR(dev)    ((dev)>>8)        // 从设备号中提取主设备号
#define MINOR(dev)    ((dev) & 0xff)   // 从设备号中提取次设备号
#define MKDEV(ma,mi)  ((ma)<<8 | (mi)) // 将主、次设备号拼凑为设备号
/* 只是拼凑设备号,并未注册到系统中,若要使用需要竞态申请 */

1. 静态申请设备号

/* 参数:
    dev_t from - 要申请的设备号(起始)
    unsigned count - 要申请的设备号数量
    const char *name - 设备名
   返回值:
    成功:0
    失败:负数(绝对值是错误码)
*/
/**
 * 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)
{
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}

2.  动态分配设备号

/* 参数:
    dev_t *dev - 用于保存分配到的第一个设备号(起始)
    unsigned baseminor - 起始次设备号
    unsigned count - 要分配设备号的数量
    const char *name - 设备名
   返回值:
    成功:0
    失败:负数(绝对值是错误码)
*/
/**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

3. 释放设备号

/* 参数:
    dev_t from - 要释放的第一个设备号(起始)
    unsigned count - 要释放的次设备号数量 
*/
/**
 * unregister_chrdev_region() - unregister a range of device numbers
 * @from: the first in the range of numbers to unregister
 * @count: the number of device numbers to unregister
 *
 * This function will unregister a range of @count device numbers,
 * starting with @from.  The caller should normally be the one who
 * allocated those numbers in the first place...
 */
void unregister_chrdev_region(dev_t from, unsigned count)
{
	dev_t to = from + count;
	dev_t n, next;

	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
}


4. 创建设备文件:

利用cat /proc/devices查看申请到的设备名,设备号。

  1. 使用mknod手工创建:mknod filename type major minor
  2. 自动创建设备节点:利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

详细解析见: Linux设备文件自动生成

 

2.字符设备结构体struct cdev常用操作函数

在Linux内核中,使用 struct cdev 来描述一个字符设备

<include/linux/cdev.h>  

struct cdev {   
  struct kobject kobj;                  //内嵌的内核对象.  
  struct module *owner;                 //该字符设备所在的内核模块(所有者)的对象指针,一般为THIS_MODULE主要用于模块计数  
  const struct file_operations *ops;    //该结构描述了字符设备所能实现的操作集(打开、关闭、读/写、...),是极为关键的一个结构体
  struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表
  dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成(如果是一次申请多个设备号,此设备号为第一个)
  unsigned int count;                   //隶属于同一主设备号的次设备号的个数
} __randomize_layout;;

1. cdev_alloc():

动态分配一个cdev结构体,成功返回指向cdev结构体的指针,失败返回NULL。

/* 返回值:
    成功 cdev 对象首地址
    失败:NULL */
/**
 * cdev_alloc() - allocate a cdev structure
 *
 * Allocates and returns a cdev structure, or NULL on failure.
 */
struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

cdev_alloc()只能创建原始的cdev结构体,实际中,一般我们很少直接使用cdev结构体,而是创建我们需要的结构体,这个结构体中除了包含了cdev成员,还包含一些其他需要的信息。例如

struct mycdev
{
    struct cdev cdev;
    int otherdata;
    ....
};

 仿照struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);给我们的结构体分配内存。

2. cdev_init():

初始化cdev的成员,将字符设备(cdev)和文件操作函数(file_operation)关联起来。

/**
 * 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)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

可以看到在cdev_init()函数中,只是初始化了cdev的kobj,ops,list三个成员;并没有初始化owner,dev,count,那这三个成员是在哪里初始化的了?答案是:在cdev_add()函数当中初始化的。

3. cdev_add():

注册cdev设备对象(添加到系统字符设备列表chrdevs 中)。将字符设备注册到内核,在将设备注册到内核后,设备就活了。count一般取1,有多个子设备的时候,要循环cdev/_add()。


/**
 * 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.
 */
/* 参数:
    struct cdev *p - 被注册的cdev对象
    dev_t dev - 设备的第一个设备号
    unsigned - 这个设备连续的次设备号数量
   返回值:
    成功:0
    失败:负数(绝对值是错误码)
*/
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);
	if (error)
		return error;

	kobject_get(p->kobj.parent);

	return 0;
}

可以看到在将字符设备注册到内核的时候,设置了dev,count变量。而owner没有设置,我们一般自己手动设置为

dev->cdev.owner=THIS_MODULE;

4. cdev_del()

将cdev设备从系统中移除(注销 )


/**
 * 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.
 *
 * NOTE: This guarantees that cdev device will no longer be able to be
 * opened, however any cdevs already open will remain and their fops will
 * still be callable even after cdev_del returns.
 */
void cdev_del(struct cdev *p)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

3、struct cdev 中的 file_operations *fops成员

Linux下一切皆是“文件”,字符设备也是这样,file_operations结构体中的成员函数是字符设备程序设计的主题,这些函数实际会在用户层程序进行Linux的open()、close()、write()、read()等系统调用时最终被调用。对字符设备操作,其实就是在实现这几个函数。


struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
			u64);
} __randomize_layout;

 

四、简单字符设备实例

内核空间:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <asm/current.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
static int major = 0; 
static int minor = 0; 
const int count = 3; 
#define DEVNAME "demo" 
static struct cdev *demop = NULL; 
//打开设备 
static int demo_open(struct inode *inode, struct file *filp) 
{ 
  //get command and pid 
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__); 
  //get major and minor from inode 
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__); 
  return 0; 
} 
//关闭设备 
static int demo_release(struct inode *inode, struct file *filp) 
{ 
  //get command and pid 
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__); 
  //get major and minor from inode 
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__); 
  return 0; 
} 
//读设备
static ssize_t demo_read(struct file *filp, char __user *buf, size_t size, loff_t *offset) 
{
  struct inode *inode = filp->f_path.dentry->d_inode; 
  //get command and pid 
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__); 
  //get major and minor from inode 
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__); 
  return 0; 
} 
//写设备 
static ssize_t demo_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset) 
{ 
  struct inode *inode = filp->f_path.dentry->d_inode; 
  //get command and pid 
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__); 
  //get major and minor from inode 
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__); 
  return 0; 
} 
//操作方法集 
static struct file_operations fops = {
  .owner = THIS_MODULE, .open = demo_open, 
  .release= demo_release, 
  .read = demo_read, 
  .write = demo_write, 
}; 
//cdev设备模块初始化 
static int __init demo_init(void) 
{ 
  dev_t devnum; int ret; 
  //get command and pid 
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__); 
  //1. alloc cdev obj 
  demop = cdev_alloc(); 
  if(NULL == demop) { 
    return -ENOMEM; 
  }
    //2. init cdev obj
    cdev_init(demop, &fops);
    ret = alloc_chrdev_region(&devnum, minor, count, DEVNAME);
    if(ret){
        goto ERR_STEP;
    }
    major = MAJOR(devnum);
    //3. register cdev obj
    ret = cdev_add(demop, devnum, count);
    if(ret){
        goto ERR_STEP1;
    }
    //get command and pid
    printk(KERN_INFO "(%s:pid=%d), %s : %s : %d - ok.\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
    return 0;

ERR_STEP1:
    unregister_chrdev_region(devnum, count);

ERR_STEP:
    cdev_del(demop);
    //get command and pid
    printk(KERN_INFO "(%s:pid=%d), %s : %s : %d - fail.\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
    return ret;
}

static void __exit demo_exit(void)
{
    //get command and pid
    printk(KERN_INFO "(%s:pid=%d), %s : %s : %d - leave.\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
    unregister_chrdev_region(MKDEV(major, minor), count);
    cdev_del(demop);
}

module_init(demo_init);
module_exit(demo_exit);

用户空间:

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

int main(int num, char *arg[])
{
    if(2 != num){
        printf("Usage: %s /dev/devfile\n", arg[0]);
        return -1;
    }
    int fd = open(arg[1], O_RDWR);
    if(0 > fd){
        perror("open");
        return -1;
    }
    getchar();
    int ret = read(fd, 0x321, 0);
    printf("read: ret = %d.\n", ret);
    getchar();
    ret = write(fd, 0x123, 0);
    printf("write: ret = %d.\n", ret);
    getchar();
    close(fd);
    return 0;
}

然后用cat /proc/devices 查看,会发现设备号已经申请成功;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值