Linux 驱动开发 三:字符设备驱动框架

一、参考

(3条消息) Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析_知秋一叶-CSDN博客

(3条消息) linux设备驱动框架_不忘初心-CSDN博客_linux设备驱动

(3条消息) linux字符驱动框架_daha1314的博客-CSDN博客_linux字符驱动框架

(3条消息) Linux驱动开发16之再论register_chrdev_region_wangdapao12138的博客-CSDN博客

29.使用register_chrdev_region()系列来注册字符设备 - 诺谦 - 博客园 (cnblogs.com)

正点原子相关资料

二、概述

驱动编写注意事项:编译驱动的时候需要用到 Linux 内核源码。因此需要解压缩 Linux 内核源码并编译 Linux 内核源码。

Linux 驱动开发 三:字符设备驱动框架_第1张图片
Linux 内核中:

1、使用 cdev 结构体来描述字符设备

2、通过其成员 dev_t 来定义设备号(分为设备号)以确定字符设备的唯一性

3、通过其成员 file_operations 来定义字符设备驱动提供给 VFS接口函数,如常见的 open()read()write() 等;

Linux 字符设备驱动中:

1、模块加载函数获取设备号

  • register_chrdev( ):注册字符设备(linux 版本 2.4 之前注册方式)。

  • register_chrdev_region( ):静态获取设备号

  • alloc_chrdev_region( ):动态获取设备号

2、通过 cdev_init( ) 建立 cdevfile_operations 之间的连接,通过 cdev_add( ) 向系统添加一个 cdev 以完成注册;

3、模块卸载函数通过 cdev_del( ) 来注销 cdev

4、释放设备号;

  • unregister_chrdev( ):注销字符设备(linux 版本 2.4 之前注销方式)。
  • unregister_chrdev_region( ):注销字符设备。

用户空间访问该设备的程序:

1、通过 Linux 系统调用,如 open( )read( )write( )来调用 file_operations 结构体变量中定义字符设备驱动提供给VFS的接口函数;

三、cdev

<include/linux/cdev.h>

struct cdev {
	struct kobject kobj;				// 内嵌的内核对象
	struct module *owner;				// 该字符设备所在的内核模块的对象指针
	const struct file_operations *ops;	// 该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
	struct list_head list;				// 该结构描述了字符设备所能实现的方法,是极为关键的一个结构体
	dev_t dev;							// 字符设备的设备号,由主设备号和次设备号构成
	unsigned int count;					// 隶属于同一主设备号的次设备号的个数
};

内核给出的操作 struct cdev 结构的接口主要有以下几个:

函数 说明
cdev_init( ) 主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接
cdev_alloc( ) 主要分配一个struct cdev结构**,**动态申请一个cdev内存,并做了cdev_init中所做的初始化工作(有一点差异)
cdev_add( ) 向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了
cdev_del( ) 向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了
cdev_put( )

1、cdev_init

/**
 * 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;
}

该函数主要对 struct cdev 结构体做初始化最重要的就是建立 cdevfile_operations 之间的连接:

cdev_init 作用:

1、将整个结构体清零;

2、初始化 list 成员使其指向自身;

3、初始化 kobj 成员;

4、初始化 ops 成员;

2、cdev_alloc

/**
 * 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;
}

该函数主要分配一个 struct cdev 结构动态申请一个 cdev 内存,并做了 cdev_init 中所做的前面 3 步初始化工作(第四步初始化工作需要在调用 cdev_alloc 后,显式的做初始化即: .ops=xxx_ops)。

在上面的两个初始化的函数中,我们没有看到关于 owner 成员dev 成员count 成员初始化;其实,owner 成员的存在体现驱动程序内核模块间的亲密关系struct module内核对于一个模块的抽象,该成员在字符设备中可以体现设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而 dev 成员和 count 成员则在 cdev_add才会赋上有效的值

3、cdev_add

/**
 * cdev_add() - add a char device to the system
 * @p: the cdev structure for the device 设备的cdev结构
 * @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)
{
	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;
}

该函数向内核注册一个 struct cdev 结构,即正式通知内核由 struct cdev *p 代表的字符设备已经可以使用了

4、cdev_del

/**
 * 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)
{
	cdev_unmap(p->dev, p->count);
	kobject_put(&p->kobj);
}

该函数向内核注销一个 struct cdev 结构,即正式通知内核由 struct cdev *p 代表的字符设备已经不可以使用了

四、设备号操作

1、设备号

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

typedef unsigned int __u32;			// 定义在 include/uapi/asm-generic/int-ll64.h 头文件中

typedef __u32 __kernel_dev_t;

typedef __kernel_dev_t		dev_t;  // 定义在 include/linux/types.h 头文件中

在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号

// include/linux/kdev_t.h

#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))	// 从设备号中提取 major
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))	// 从设备号中提取 minor
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))		// 通过 major 和 minor 构建设备号

#define print_dev_t(buffer, dev)					\
	sprintf((buffer), "%u:%u\n", MAJOR(dev), MINOR(dev))

#define format_dev_t(buffer, dev)					\
	({								\
		sprintf(buffer, "%u:%u", MAJOR(dev), MINOR(dev));	\
		buffer;							\
	})

:这只是构建设备号并未注册,需要调用 register_chrdev_region 静态申请;

2、char_device_struct

#define CHRDEV_MAJOR_HASH_SIZE	255

static struct char_device_struct {
	struct char_device_struct *next;
	unsigned int major;
	unsigned int baseminor;
	int minorct;
	char name[64];
	struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

内核中的每一个字符设备驱动程序都由一个 char_device_struct 结构体来描述,包含主设备号起始次设备号次设备号个数等信息。

内核使用 chrdevs 这个指针数组来管理所有的字符设备驱动程序数组范围 0-255 ,看上去好像还是只支持 256 个字符设备驱动程序,其实并不然,每一个 char_device_struct 结构都含有一个 next 指针,它可以指向与其主设备号相同其它字符设备驱动程序,它们之间主设备号相同,各自的次设备号范围相互不重叠
Linux 驱动开发 三:字符设备驱动框架_第2张图片

3、分配设备号

1、register_chrdev

/* 但其实这个函数是linux版本2.4之前的注册方式,它的原理是:
 * 1、确定一个主设备号,如果major=0,则会自动分配设备号(主设备号下所有此设备号都会被注册)
 * 2、构造一个file_operations结构体, 然后放在chrdevs数组中
 * 3、注册 --》
 * register_chrdev,cat /proc/devices查看内核中已经注册过的字符设备驱动(和块设备驱动),
 * 注意这里并不是驱动文件设备节点!
 * 当读写字符设备的时候,就会根据主设备号从chrdevs数组中取出相应的结构体,并调用相应的处理函数
 * 缺点:
 * 每注册一个字符设备,还会连续注册0~255个次设备号,使它们绑定在同一个 file_operations 操作方法结构体上,
 * 在大多数情况下,都只用极少的次设备号,所以会浪费很多资源。
 */
static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
{
	return __register_chrdev(major, 0, 256, name, fops);
}
/**
 * __register_chrdev() - create and register a cdev occupying a range of minors
 * @major: major device number or 0 for dynamic allocation
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: name of this range of devices
 * @fops: file operations associated with this devices
 *
 * If @major == 0 this functions will dynamically allocate a major and return
 * its number.
 *
 * If @major > 0 this function will attempt to reserve a device with the given
 * major number and will return zero on success.
 *
 * Returns a -ve errno on failure.
 *
 * The name of this device has nothing to do with the name of the device in
 * /dev. It only helps to keep track of the different owners of devices. If
 * your module name has only one type of devices it's ok to use e.g. the name
 * of the module here.
 */
int __register_chrdev(unsigned int major, unsigned int baseminor,
		      unsigned int count, const char *name,
		      const struct file_operations *fops)
{
	struct char_device_struct *cd;
	struct cdev *cdev;
	int err = -ENOMEM;

	cd = __register_chrdev_region(major, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);

	cdev = cdev_alloc();
	if (!cdev)
		goto out2;

	cdev->owner = fops->owner;
	cdev->ops = fops;
	kobject_set_name(&cdev->kobj, "%s", name);

	err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);
	if (err)
		goto out;

	cd->cdev = cdev;

	return major ? 0 : cd->major;
out:
	kobject_put(&cdev->kobj);
out2:
	kfree(__unregister_chrdev_region(cd->major, baseminor, count));
	return err;
}

register_chrdev 调用了 __register_chrdev_region强制指定起始次设备号0256个把一个主设备号下的所有次设备号都申请光了。同时它还封装cdev_initcdev_add

2.4 的内核我们使用 register_chrdev(0, "hello", &hello_fops) 来进行字符设备设备节点的分配,这种方式每一个主设备号只能存放一种设备,它们使用相同file_operation 结构体,也就是说内核最多支持 256 个字符设备驱动程序。

2、register_chrdev_region

/**
 * 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.
 * 
 * from: 注册的指定起始设备编号,比如:MKDEV(100, 0),表示起始主设备号100, 起始次设备号为0
 * count:需要连续注册的次设备编号个数,
 *       比如: 起始次设备号为0,count=100,表示0~99的次设备号都要绑定在同一个file_operations操作方法结构体上
 * name:字符设备名称
 */
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.6 的内核之后,新增了一个 register_chrdev_region 函数,它支持同一个主设备号下的次设备号进行分段,每一段供给一个字符设备驱动程序使用,使得资源利用率大大提升,同时,2.6 的内核保留了原有的 register_chrdev 方法。在 2.6 的内核中这两种方法都会调用__register_chrdev_region 函数。

register_chrdev_region 则是根据要求的范围进行申请,同时我们需要手动 cdev_initcdev_add

3、alloc_chrdev_region

/**
 * 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.
 *
 * dev :alloc_chrdev_region函数向内核申请下来的设备号
 * baseminor :次设备号的起始
 * count: 申请次设备号的个数
 * name :执行 cat /proc/devices显示的名称
 */
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;
}

让内核分配给我们一个尚未使用的主设备号,不是由我们自己指定的。

4、__register_chrdev_region

/*
 * Register a single major with a specified minor range.
 *
 * If major == 0 this functions will dynamically allocate a major and return
 * its number.
 *
 * If major > 0 this function will attempt to reserve the passed range of
 * minors and will return zero on success.
 *
 * Returns a -ve errno on failure.
 */
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
			   int minorct, const char *name)
{
	struct char_device_struct *cd, **cp;
	int ret = 0;
	int i;

	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);

	mutex_lock(&chrdevs_lock);

	/* temporary */
	if (major == 0) {
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}

		if (i == 0) {
			ret = -EBUSY;
			goto out;
		}
		major = i;
	}

	cd->major = major;
	cd->baseminor = baseminor;
	cd->minorct = minorct;
	strlcpy(cd->name, name, sizeof(cd->name));

	i = major_to_index(major);

	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major > major ||
		    ((*cp)->major == major &&
		     (((*cp)->baseminor >= baseminor) ||
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
			break;

	/* Check for overlapping minor ranges.  */
	if (*cp && (*cp)->major == major) {
		int old_min = (*cp)->baseminor;
		int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
		int new_min = baseminor;
		int new_max = baseminor + minorct - 1;

		/* New driver overlaps from the left.  */
		if (new_max >= old_min && new_max <= old_max) {
			ret = -EBUSY;
			goto out;
		}

		/* New driver overlaps from the right.  */
		if (new_min <= old_max && new_min >= old_min) {
			ret = -EBUSY;
			goto out;
		}
	}

	cd->next = *cp;
	*cp = cd;
	mutex_unlock(&chrdevs_lock);
	return cd;
out:
	mutex_unlock(&chrdevs_lock);
	kfree(cd);
	return ERR_PTR(ret);
}

具体分析见:(3条消息) 浅析字符设备驱动程序__register_chrdev_region_漫不经心-CSDN博客

5、unregister_chrdev

/**
 * 注销设备驱动程序的内核函数
 * major:主设备号
 * name:设备文件
 */
static inline void unregister_chrdev(unsigned int major, const char *name)
{
	__unregister_chrdev(major, 0, 256, name);
}
/**
 * __unregister_chrdev - unregister and destroy a cdev
 * @major: major device number
 * @baseminor: first of the range of minor numbers
 * @count: the number of minor numbers this cdev is occupying
 * @name: name of this range of devices
 *
 * Unregister and destroy the cdev occupying the region described by
 * @major, @baseminor and @count.  This function undoes what
 * __register_chrdev() did.
 */
void __unregister_chrdev(unsigned int major, unsigned int baseminor,
			 unsigned int count, const char *name)
{
	struct char_device_struct *cd;

	cd = __unregister_chrdev_region(major, baseminor, count);
	if (cd && cd->cdev)
		cdev_del(cd->cdev);
	kfree(cd);
}

6、unregister_chrdev_region

/**
 * unregister_chrdev_region() - return 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));
	}
}
static struct char_device_struct *
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
{
	struct char_device_struct *cd = NULL, **cp;
	int i = major_to_index(major);

	mutex_lock(&chrdevs_lock);
	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major == major &&
		    (*cp)->baseminor == baseminor &&
		    (*cp)->minorct == minorct)
			break;
	if (*cp) {
		cd = *cp;
		*cp = cd->next;
	}
	mutex_unlock(&chrdevs_lock);
	return cd;
}

五、file_operations

struct file_operations {
	struct module *owner;	//拥有该结构的模块的指针,一般为THIS_MODULES
	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 *);
	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 *);
	int (*mremap)(struct file *, struct vm_area_struct *);
	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 (*aio_fsync) (struct kiocb *, int datasync); //刷新待处理的数据
	int (*fasync) (int, struct file *, int); //通知设备FASYNC标志发生变化
	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
};

六、字符设备驱动开发步骤

Linux一切皆为文件,驱动加载成功以后会在 “/dev” 目录下生成一个相应的文件,应用程序通过对这个名为 “/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作

应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间

1、驱动模块的加载和卸载

module_init(xxx_init); //注册模块加载函数 
module_exit(xxx_exit); //注册模块卸载函数

module_init() 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数,当使用 “insmod” 命令加载驱动的时候,xxx_init 这个函数就会被调用。

module_exit() 函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数,当使 用 “rmmod” 命令卸载具体驱动的时候 xxx_exit 函数就会被调用。

字符设备驱动模块加载和卸载模板如下所示:

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
}

// __init、__exit 定义在 inlucde/linux/init.h

// 声明在 inlucde/linux/init.h
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

驱动编译完成以后扩展名.ko,有两种命令可以加载驱动模块insmodmodprobeinsmod最简单模块加载命令,此命令用于加载指定的 .ko 模块,比如加载 drv.ko 这个驱动模块,命 令如下:

insmod drv.ko

insmod 命令不能解决模块的依赖关系,比如 drv.ko 依赖 first.ko 这个模块,就必须先使用 insmod 命令加载 first.ko 这个模块,然后再加载 drv.ko 这个模块。

但是 modprobe不会存在这个问题modprobe分析模块的依赖关系然后将所有的依赖模块都加载到内核中,因此 modprobe 命令相比 insmod智能一些。modprobe 命令主要智能在提供了模块的依赖性分析错误检查错误报告等功能,推荐使用modprobe 命令来加载驱动modprobe 命令默认会去 /lib/modules/ 目录中查找模块,比如本书使用的 Linux kernel 的版本号为 4.1.15, 因此modprobe 命令默认会到 /lib/modules/4.1.15 这个目录中查找相应的驱动模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建

驱动模块卸载使用命令 “rmmod” 即可,比如要卸载 drv.ko,使用如下命令即可:

rmmod drv.ko

也可以使用 “modprobe -r” 命令卸载驱动,比如要卸载 drv.ko,命令如下:

modprobe -r drv.ko

使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块前提这些依赖模块已经没有被其他模块所使用,否则就不能使用modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令。

2、字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册注销函数原型如下所示:

static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:

  • major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号次设备号两部分。
  • name:设备名字,指向一串字符串。
  • fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。

unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:

  • major:要注销的设备对应的主设备号
  • name:要注销的设备对应的设备名

一般字符设备注册在驱动模块的入口函数 xxx_init( )进行字符设备的注销在驱动模块的出口函数 xxx_exit( ) 中进行。模板如下:

static struct file_operations test_fops;

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    
    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0){
        /* 字符设备注册失败,自行处理 */
    }
    
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

3、实现设备的具体操作函数

file_operations 结构体就是设备的具体操作函数,我们定义了 file_operations 结构体类型的变量 test_fops,但是还没对其进行初始化,也就是初始化其中的 openreleasereadwrite 等具体的设备操作函数。

添加基本操作,添加后内容如下:

/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能 */
    return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能 */
    return 0;
}

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    
    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0){
        /* 字符设备注册失败,自行处理 */
    }
    
    return 0;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

4、添加LICENSE 和作者信息

我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE必须添加的否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用 如下两个函数:

MODULE_LICENSE() //添加模块LICENSE信息
MODULE_AUTHOR()  //添加模块作者信息

添加后内容如下:

……

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);

// 声明在 inlucde/linux/module.h
/* LICENSE 采用GPL 协议 */
MODULE_LICENSE("GPL");

七、测试

我们以 chrdevbase 这个虚拟设备为例,完整编写一个字符设备驱动模块chrdevbase 不是实际存在的一个设备,是为了方便说明字符设备的开发而引入的一个虚拟设备。

1、创建VSCode 工程

Ubuntu创建一个目录用来存放 Linux 驱动程序,比如我创建了一个名为 linux_driver 的目录来存放所有的 Linux 驱动。

onlylove@ubuntu:~/linux/driver/linux_driver$ pwd
/home/onlylove/linux/driver/linux_driver

linux_driver 目录下新建一个名为 1_chrdevbase 的子目录来存放本实验所有文件。

onlylove@ubuntu:~/linux/driver/linux_driver$ mkdir 1_chrdevbase
onlylove@ubuntu:~/linux/driver/linux_driver$ ls
1_chrdevbase
onlylove@ubuntu:~/linux/driver/linux_driver$

1_chrdevbase 目录中新建 VSCode 工程:

1、保存工程文件 1_chrdevbase.code-workspace

2、新建 chrdevbase.c 文件

onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ pwd
/home/onlylove/linux/driver/linux_driver/1_chrdevbase
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls -al
total 12
drwxrwxr-x 2 onlylove onlylove 4096 Nov 29 04:31 .
drwxrwxr-x 3 onlylove onlylove 4096 Nov 29 04:26 ..
-rw-rw-r-- 1 onlylove onlylove   60 Nov 29 04:31 1_chrdevbase.code-workspace
-rw-rw-r-- 1 onlylove onlylove    0 Nov 29 04:29 chrdevbase.c
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$

2、添加头文件路径

因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux 源码中的头文件路径。打开 VSCode,按下 “Crtl+Shift+P” 打开 VSCode 的控制台,然后输入 “C/C++: Edit configurations(JSON) ”,打开 C/C++ 编辑配置文件,如图所示:
在这里插入图片描述
打开以后会自动.vscode 目录下生成一个名为 c_cpp_properties.json 的文件,此文件默认内容如下所示:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

includePath 表示头文件路径,需要将 Linux 源码里面的头文件路径添加进来,也就是我们前面移植的 Linux 源码中的头文件路径。

需要添加路径:

1、linux/include

2、linux//arch/arm/include

3、linux/arch/arm/include/gener ated

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

注意,这里使用了绝对路径。

3、编写 Makefile

1、指定内核路径,使用 KERNELDIR 变量。

2、指定输入文件,使用 CURRENT_PATH 变量。

3、编译为模块,使用 obj-m 说明。

4、将当前工作目录转移到你所指定的位置,使用 -C 选项。

5、当用户需要以某个内核为基础编译一个外部模块的话,使用 M 选项。

KERNELDIR := /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

4、编写实验程序

1、驱动模块加载和卸载测试

#include "linux/init.h"
#include "linux/module.h"

/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
    /* 出口函数具体内容 */
}

// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");

make 编译过程如下:

onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ make
make -C /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/home/onlylove/linux/driver/linux_driver/1_chrdevbase modules
make[1]: Entering directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
  CC [M]  /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.mod.o
  LD [M]  /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.ko
make[1]: Leaving directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$

加载 .ko 文件,测试是否成功,测试过程如下:

/ # ls
bin            etc            mnt            sbin           usr
chrdevbase.ko  lib            proc           sys
dev            linuxrc        root           tmp
/ # lsmod
Module                  Size  Used by    Not tainted
/ # insmod chrdevbase.ko
/ # lsmod
Module                  Size  Used by    Tainted: G
chrdevbase               586  0
/ # rmmod chrdevbase.ko
/ # lsmod
Module                  Size  Used by    Tainted: G
/ #

2、字符设备注册与注销测试

#include "linux/init.h"
#include "linux/module.h"
#include "linux/fs.h"

// 声明在 linux/fs.h 头文件中
static struct file_operations fops;

/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    // 声明在 linux/fs.h 头文件中
    retvalue = register_chrdev(200,"chrdev",&fops);
    if(retvalue < 0){
        /* 字符设备注册失败 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
    /* 出口函数具体内容 */
    // 声明在 linux/fs.h 头文件中
    unregister_chrdev(200,"chrdev");
}

// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");

加载 .ko 文件,测试是否成功,测试过程如下:

/ # ls
bin            etc            mnt            sbin           usr
chrdevbase.ko  lib            proc           sys
dev            linuxrc        root           tmp
/ # lsmod
Module                  Size  Used by    Tainted: G
/ # cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
207 ttymxc
216 rfcomm
226 drm
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc

Block devices:
  1 ramdisk
259 blkext
  7 loop
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ #
/ # insmod chrdevbase.ko
/ # lsmod
Module                  Size  Used by    Tainted: G
chrdevbase               832  0
/ # cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
200 chrdev
207 ttymxc
216 rfcomm
226 drm
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc

Block devices:
  1 ramdisk
259 blkext
  7 loop
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ # rmmod chrdevbase.ko
/ # lsmod
Module                  Size  Used by    Tainted: G
/ # cat proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
207 ttymxc
216 rfcomm
226 drm
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc

Block devices:
  1 ramdisk
259 blkext
  7 loop
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ #

通过以上日志可以看到,使用 insmod 加载模块后,/proc/devices 下注册 200 chrdev。使用 rmmod 卸载模块后,/proc/devices 下销毁 200 chrdev

3、实现具体操作测试

具体操作就是使用自定义函数,将函数指针赋值到 file_operations 结构体中。

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 *);
	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 *);
	int (*mremap)(struct file *, struct vm_area_struct *);
	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 (*aio_fsync) (struct kiocb *, 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
};

核心:对 file_operations 结构体变量进行赋值。

#include "linux/init.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/types.h"

// struct inode 声明在 linux/fs.h 中
// struct file 声明在 linux/fs.h 中
int chrdev_open (struct inode *i, struct file *f)
{
    // printk 声明在 linux/printk.h 中
    printk("chrdevbase open!\r\n");
    return 0;
}

int chrdev_release (struct inode *i, struct file *f)
{
    printk("chrdevbase release!\r\n");
    return 0;
}

// ssize_t 定义在 linux/types.h 中
// __user 定义在 linux/compiler.h 中
// size_t 定义在 linux/types.h 中
// loff_t 定义在 linux/types.h 中
ssize_t chrdev_read (struct file *f, char __user *b, size_t c, loff_t * l)
{
    printk("chrdevbase read!\r\n");
    return 0;
}

ssize_t chrdev_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
    printk("chrdevbase write!\r\n");
    return 0;
}

// 声明在 linux/fs.h 头文件中
static struct file_operations fops = {
    .open = chrdev_open,
    .release = chrdev_release,
    .read = chrdev_read,
    .write = chrdev_write,
};

/* 驱动入口函数 */
static int __init chrdevbase_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    // 声明在 linux/fs.h 头文件中
    retvalue = register_chrdev(200,"chrdev",&fops);
    if(retvalue < 0){
        /* 字符设备注册失败 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit chrdevbase_exit(void)
{
    /* 出口函数具体内容 */
    // 声明在 linux/fs.h 头文件中
    unregister_chrdev(200,"chrdev");
}

// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");

特别说明

这里使用了 printk输出信息, 而不是 printf!因为在 Linux 内核中没有 printf 这个函数。printk 相当于 printf 的孪生兄妹,printf 运行在用户态printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用 printk 这个函数。不同之处在于,printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面,定义如下:

#define KERN_SOH	"\001"		/* ASCII Start Of Header */
#define KERN_SOH_ASCII	'\001'

#define KERN_EMERG	KERN_SOH "0"	/* system is unusable 紧急事件,一般是内核崩溃 */
#define KERN_ALERT	KERN_SOH "1"	/* action must be taken immediately 必须立即采取行动 */
#define KERN_CRIT	KERN_SOH "2"	/* critical conditions 临界条件,比如严重的软件或硬件错误 */
#define KERN_ERR	KERN_SOH "3"	/* error conditions 错误状态,一般设备驱动程序中使用 KERN_ERR报告硬件错误 */
#define KERN_WARNING	KERN_SOH "4"	/* warning conditions 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE	KERN_SOH "5"	/* normal but significant condition 有必要进行提示的一些信息 */
#define KERN_INFO	KERN_SOH "6"	/* informational 提示性的信息 */
#define KERN_DEBUG	KERN_SOH "7"	/* debug-level messages 调试信息 */

一共定义了 8 个级别,其中 0 的优先级最高,7 的优先级最低。如果要设置消息级别,参考如下示例:

printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");

具体操作函数需要通过应用层函数调用完成测试

4、编写测试app

1、编译驱动模块

onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace  chrdevbase_app.c  chrdevbase.c  Makefile
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ make
make -C /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/home/onlylove/linux/driver/linux_driver/1_chrdevbase modules
make[1]: Entering directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
  CC [M]  /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.mod.o
  LD [M]  /home/onlylove/linux/driver/linux_driver/1_chrdevbase/chrdevbase.ko
make[1]: Leaving directory '/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace  chrdevbase.c   chrdevbase.mod.c  chrdevbase.o  modules.order
chrdevbase_app.c             chrdevbase.ko  chrdevbase.mod.o  Makefile      Module.symvers
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ cp chrdevbase.ko /home/onlylove/linux/nfs/rootfs-l/chrdevbase.ko

2、编写 app 程序

#include 
#include 
#include 
#include "stdio.h"

int main(int argc, char *argv[])
{
    int fd = 0, retvalue = 0;
    char readbuf[100] = "", writebuf[100] = "";
    fd = open(argv[1],O_RDWR);
    if(fd < 0){
        printf("Can't open file %s\r\n", argv[1]);
        return -1;
    }
    read(fd, readbuf, 50);
    write(fd, writebuf, 50);
    retvalue = close(fd);
    if(retvalue < 0){
        printf("Can't close file %s\r\n", argv[1]);
        return -1;
    }

    return 0;
}

3、编译 app 程序

arm-linux-gnueabihf-gcc chrdevbase_app.c -o chrdevbase_app
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace  chrdevbase.c   chrdevbase.mod.c  chrdevbase.o  modules.order
chrdevbase_app.c             chrdevbase.ko  chrdevbase.mod.o  Makefile      Module.symvers
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ 
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ 
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ 
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ arm-linux-gnueabihf-gcc chrdevbase_app.c -o chrdevbase_app
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ ls
1_chrdevbase.code-workspace  chrdevbase_app.c  chrdevbase.ko     chrdevbase.mod.o  Makefile       Module.symvers
chrdevbase_app               chrdevbase.c      chrdevbase.mod.c  chrdevbase.o      modules.order
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$
onlylove@ubuntu:~/linux/driver/linux_driver/1_chrdevbase$ cp chrdevbase_app /home/onlylove/linux/nfs/rootfs-l/chrdevbase_app

5、加载驱动模块

/ # ls
bin             etc             proc            tmp
chrdevbase.ko   lib             root            usr
chrdevbase_app  linuxrc         sbin
dev             mnt             sys
/ # lsmod
Module                  Size  Used by    Tainted: G
/ # insmod chrdevbase.ko
/ # lsmod
Module                  Size  Used by    Tainted: G
chrdevbase              1282  0
/ # cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
200 chrdev
207 ttymxc
216 rfcomm
226 drm
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc

Block devices:
  1 ramdisk
259 blkext
  7 loop
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ #

6、创建设备节点文件

驱动加载成功需要在 /dev 目录下创建一个与之对应的设备节点文件应用程序就是通过操作这个设备节点文件完成对具体设备的操作。输入如下命令创建 /dev/chrdevbase 这个设备节 点文件:

mknod /dev/chrdevbase c 200 0

其中 “mknod”创建节点命令“/dev/chrdevbase” 是要创建的节点文件“c” 表示这是个字符设备“200” 是设备的主设备号“0” 是设备的次设备号。创建完成以后就会存在 /dev/chrdevbase 这个文件,可以使用 “ls /dev/chrdevbase -l” 命令查看,结果下所示:

/dev # ls /dev/chrdevbase -l
crw-r--r--    1 0        0         200,   0 Jan  1 01:38 /dev/chrdevbase

测试过程如下:

/dev # ls /dev/chrdevbase -l
ls: /dev/chrdevbase: No such file or directory
/dev # mknod /dev/chrdevbase c 200 0
/dev # ls /dev/chrdevbase -l
crw-r--r--    1 0        0         200,   0 Jan  1 01:38 /dev/chrdevbase
/dev #

如果 chrdevbase_app 想要读写 chrdevbase 设备,直接对 /dev/chrdevbase 进行读写操作即可。相当于 /dev/chrdevbase 这个文件是 chrdevbase 设备在用户空间中的实现。

7、chrdevbase 设备操作测试

使用 chrdevbase_app 软件操作 chrdevbase 这个设备,看看读写是否正常,首先进行读操作,输入如下命令:

./chrdevbase_app /dev/chrdevbase

测试结果如下:

/ # ls
bin             etc             proc            tmp
chrdevbase.ko   lib             root            usr
chrdevbase_app  linuxrc         sbin
dev             mnt             sys
/ # ./chrdevbase_app /dev/chrdevbase
chrdevbase open!
chrdevbase read!
chrdevbase write!
chrdevbase release!
/ #

8、卸载驱动模块

如果不再使用某个设备的话可以将其驱动卸载掉,比如输入如下命令卸载掉 chrdevbase 这个设备:

rmmod chrdevbase.ko

卸载以后使用 lsmod 命令查看 chrdevbase 这个模块还存不存在。

测试结果如下:

/ # lsmod
Module                  Size  Used by    Tainted: G
chrdevbase              1282  0
/ # rmmod chrdevbase.ko
/ # lsmod
Module                  Size  Used by    Tainted: G
/ # ls /dev/chrdevbase -l
ls: /dev/chrdevbase: No such file or directory
/ #

你可能感兴趣的:(Linux驱动开发,linux)