前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区
编译相关篇
1. GCC编译
2. 静态链接与动态链接
3. makefile入门基础
设备驱动篇
1. 设备驱动概述
2. 内核模块_理论篇
3. 内核模块_实验篇
4. 字符设备_理论篇1
5. 字符设备_理论篇2
5. 字符设备_实验篇1
本节主要介绍如何使用一个驱动进行支持多个设备,以及使用宏定义container_of,将结构体的某个成员的地址,转换为该结构体的地址。
无
《 [野火]i.MX Linux开发实战指南》
百度
字符设备驱动程序是 以内核模块的形式存在 的
要向系统注册一个新的字符设备,需要这几样东西:
static struct cdev chr_dev;
static dev_t devno;
static struct file_operations chr_dev_fops =
{
.owner = THIS_MODULE,
.open = chr_dev_open,
.release = chr_dev_release,
.write = chr_dev_write,
.read = chr_dev_read,
};
#include
#include
#include
/* 其她头文件...... */
/* 一些驱动函数 */
static ssize_t xxx_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
}
static ssize_t xxx_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
}
static int xxx_open (struct inode *node, struct file *file)
{
}
static int xxx_close (struct inode *node, struct file *file)
{
}
/* 其它驱动函数...... */
/* 定义自己的驱动结构体 */
staticstruct file_operations xxx_drv = {
.owner = THIS_MODULE,
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_close,
/* 其它程序......... */
};
/* 驱动入口函数 */
staticint __init xxx_init(void)
{
}
/* 驱动出口函数 */
staticvoid __exit hello_exit(void)
{
}
/* 模块注册与卸载函数 */
module_init(xxx_init);
module_exit(xxx_exit);
/* 模块许可证(必选项) */
MODULE_LICENSE("GPL");

采用动态分配的方式,获取设备编号devno,次设备号为0,设备名称为DEV_NAME(可通过命令cat /proc/devices查看),DEV_CNT为当前申请设备编号个数
alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME)
关联字符设备结构体cdev与文件操作结构体file_operations
cdev_init(&chr_dev, &chr_dev_fops);
添加设备至cdev_map散列表中
cdev_add(&chr_dev, devno, DEV_CNT);

unregister_chrdev_region(devno, DEV_CNT);
cdev_del(&chr_dev);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos; //变量p记录了当前文件的读写位置
int ret;
int tmp = count ;
if(p > BUFF_SIZE) //如果超过了数据缓冲区的大小(128字节)的话,直接返回0。
return 0; //并且如果要读写的数据个数超过了数据缓冲区剩余的内容的话,则只读取剩余的内容。
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_from_user(vbuf, buf, tmp); //使用copy_from_user从用户空间拷贝tmp个字节的数据到数据缓冲区中
*ppos += tmp; //同时让文件的读写位置偏移同样的字节数。
return tmp;
}
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
if (p >= BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}
#include
#include
#include
#include
char *wbuf = "Hello World\n";
char rbuf[128];
int main(void)
{
printf("EmbedCharDev test\n");
//打开文件
int fd = open("/dev/chrdev", O_RDWR);
//写入数据
write(fd, wbuf, strlen(wbuf));
//写入完毕,关闭文件
close(fd);
//打开文件
fd = open("/dev/chrdev", O_RDWR);
//读取文件内容
read(fd, rbuf, 128);
//打印读取的内容
printf("The content : %s", rbuf);
//读取完毕,关闭文件
close(fd);
return 0;
}
在Linux内核中,主设备号用于 标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务。
但是,次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。
如何能够用一个驱动程序去控制各种设备呢?
很明显,首先,我们可以根据次设备号,来区分各种设备;
其次,就是前文提到过的file结构体的私有数据成员private_data。
我们可以通过该成员来做文章,不难想到为什么只有open函数和close函数的形参才有file结构体,
因为驱动程序第一个执行的是操作就是open,通过open函数就可以控制我们想要驱动的底层硬件。
一个驱动支持多个设备的具体实现方式的重点在于如何运用file的私有数据成员。
#define DEV_NAME "EmbedCharDev"
#define DEV_CNT (2)
#define BUFF_SIZE 128
/* 定义字符设备的设备号 */
static dev_t devno;
/* 定义字符设备结构体chr_dev */
static struct cdev chr_dev;
/* 数据缓冲区 */
static char vbuf1[BUFF_SIZE];
static char vbuf2[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp)
{
printk("\nopen\n ");
/* 获取该设备文件的次设备号,使用private_data指向各自的数据缓冲区 */
switch (MINOR(inode->i_rdev)) {
case 0 : {
filp->private_data = vbuf1;
break;
}
case 1 : {
filp->private_data = vbuf2;
break;
}
}
return 0;
}
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
char *vbuf = filp->private_data;
int tmp = count ;
if (p > BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_from_user(vbuf, buf, tmp);
*ppos += tmp;
return tmp;
}
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
char *vbuf = filp->private_data;
if (p >= BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}
/*虚拟字符设备*/
struct chr_dev {
struct cdev dev;
char vbuf[BUFF_SIZE];
};
/* 字符设备1 */
static struct chr_dev vcdev1;
/* 字符设备2 */
static struct chr_dev vcdev2;
static int __init chrdev_init(void)
{
int ret;
printk("4 chrdev init\n");
ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0)
goto alloc_err;
/* 关联第一个设备:vdev1 */
cdev_init(&vcdev1.dev, &chr_dev_fops);
ret = cdev_add(&vcdev1.dev, devno+0, 1);
if (ret < 0) {
printk("fail to add vcdev1 ");
goto add_err1;
}
/* 关联第二个设备:vdev2 */
cdev_init(&vcdev2.dev, &chr_dev_fops);
ret = cdev_add(&vcdev2.dev, devno+1, 1);
if (ret < 0) {
printk("fail to add vcdev2 ");
goto add_err2;
}
return 0;
/* 若虚拟设备2添加失败,则需要把虚拟设备1移除,再将申请的设备号注销 */
add_err2:
cdev_del(&(vcdev1.dev));
/* 当虚拟设备1添加失败时,直接返回的时候,只需要注销申请到的设备号即可 */
add_err1:
unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
return ret;
}
static void __exit chrdev_exit(void)
{
printk("chrdev exit\n");
unregister_chrdev_region(devno, DEV_CNT);
cdev_del(&(vcdev1.dev));
cdev_del(&(vcdev2.dev));
}
static int chr_dev_open(struct inode *inode, struct file *filp)
{
printk("open\n");
filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);
return 0;
}
static int chr_dev_release(struct inode *inode, struct file *filp)
{
printk("release\n");
return 0;
}
我们知道inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体, 我们要如何能够得到虚拟设备的数据缓冲区呢?
为此,Linux提供了一个宏定义container_of,该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。
该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。
在chr_dev_open函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针filp的私有数据成员中。
假如,我们打开虚拟设备1,那么inode->i_cdev便指向了vcdev1的成员dev,利用container_of宏, 我们就可以得到vcdev1结构体的地址,也就可以操作对应的数据缓冲区了。
==> 回顾一下inode与cdev结构体
dev_t == u32
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
struct inode {
...
dev_t i_rdev;
...
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
...
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
__u32 i_generation;
void *i_private; /* fs or device private pointer */
} __randomize_layout;
最后读写操作的相关实现如下:
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
/* 获取文件的私有数据 */
struct chr_dev *dev = filp->private_data;
char *vbuf = dev->vbuf;
int tmp = count ;
if (p > BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_from_user(vbuf, buf, tmp);
*ppos += tmp;
return tmp;
}
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
/* 获取文件的私有数据 */
struct chr_dev *dev = filp->private_data;
char *vbuf = dev->vbuf;
if (p >= BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}
在Linux内核中,主设备号用于 标识设备对应的驱动程序,告诉Linux内核使用哪一个驱动程序为该设备服务。但是,次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。
通过将我们的数据缓冲区和字符设备结构体封装到一起,由于文件结构体inode的成员i_cdev保存了对应字符设备结构体, 使用container_of宏便可以获得封装后的结构体的地址,进而得到相应的数据缓冲区。
我们知道inode中的i_cdev成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把cdev封装起来的一个结构体, 我们要如何能够得到虚拟设备的数据缓冲区呢?
为此,Linux提供了一个宏定义container_of,该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。
该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。
在chr_dev_open函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针filp的私有数据成员中。
假如,我们打开虚拟设备1,那么inode->i_cdev便指向了vcdev1的成员dev,利用container_of宏, 我们就可以得到vcdev1结构体的地址,也就可以操作对应的数据缓冲区了。