Linux
规定每一个字符设备或者块设备都必须有一个专属的设备号。一个设备号由主设备号和次设备号组成。主设备号用来表示某一类驱动,如鼠标,键盘都可以归类到USB
驱动中。而次设备号是用来表示这个驱动下的各个设备。比如第几个鼠标,第几个键盘等。
所以,我们开发字符驱动程序,申请设备号是第一步,只有有了设备号,才可以像系统注册设备。
Linux
中使用一个名为dev_t
的数据类型表示设备号。dev_t
定义在include/linux/types.h
里面。如下图所示,通过定义可以看出dev_t
是u32
类型,也就是unsigned int
类型。所以设备号是一个32位的数据类型。其中高12位为主设备号,低20位为次设备号。
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
所以,我们开发字符驱动程序,申请设备号是第一步,只有有了设备号,才可以像系统注册设备。
在文件include/linux/kdev_t.h
中提供了几个操作设备号的宏定义,如下所示:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
宏MINORBITS
表示次设备号位数,一共20位。
宏MINORMASK
用于计算次设备号使用。
宏MAJOR
表示从dev_t
中获取主设备号,本质是将dev_t
右移20位。
宏MINOR
表示从dev_t
中获取次设备号,本质是取低20位的值。
宏MKDEV
用于将主设备号ma
和次设备号mi
组成成dev_t
类型的设备号。
在编写字符设备驱动代码的时候,可以静态分配设备号或者动态分配设备号。
静态分配设备号指的是开发人员自己指定一个设备号,比如选择50这个设备号作为主设备号。因为有些设备号可能已经被系统使用了,在指定设备号的时候就不能在使用这些已经被系统使用的设备号了。使用命令cat /proc/devices
命令可以查看当前系统中已经使用了哪些设备号。
动态分配设备号指的是系统会自动给我们选择一个还没有被使用的设备号,这样就自动避免了设备号分配冲突的问题。
在内核中,提供了动态分配设备号和静态分配设备号的函数,定义在include/linux/fs.h
里面,如下表。
静态分配设备号函数 | register_chrdev_region |
---|---|
动态分配设备号函数 | alloc_chrdev_region |
register_chrdev_region
函数
函数原型:int register_chrdev_region(dev_t, unsigned, const char *);
函数参数:
参数1:设备号的起始值,类型是dev_t
。比如MKDEV(100,0)
,表示起始主设备号100,起始次设备号为0。
参数2:次设备号的数量,表示在主设备号相同的情况下有几个次设备号。
参数3:设备的名称。
函数返回值:成功返回0,失败返回值小于0
alloc_chrdev_region
函数
函数原型:int alloc_chrdev_region(dev_t*, unsigned, unsigned, const char *);
函数参数:
参数1:保存自动申请到的设备号。
参数2:次设备号的起始地址,次设备号一般从0开始,所以这个参数一般设置成0。
参数3:要申请的设备号的数量。
参数4:设备的名字。
函数返回值:成功返回0,失败返回值小于0
unregister_chrdev_region
函数
函数原型:extern void unregister_chrdev_region(dev_t, unsigned);
函数功能:设备号释放函数,注销字符设备以后要释放掉设备号。
函数参数:
参数1:要释放的设备号。
参数2:释放的设备号的数量。
dev_t.c
#include
#include
#include
#include
#include
static int major = 0;
static int minor = 0;
module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);
dev_t dev_num;
static int moduleparam_init(void)
{
int ret;
if (major)
{
printk("major is %d\n", major);
printk("minor is %d\n", minor);
dev_num = MKDEV(major, minor);
ret = register_chrdev_region(dev_num, 1, "chrdev_name");
if (ret)
{
printk("register_chrdev_region failed\n");
}
printk("register_chrdev_region succeed\n");
}
else
{
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret)
{
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d\n", major);
printk("minor is %d\n", minor);
}
return 0;
}
static void moduleparam_exit(void)
{
unregister_chrdev_region(dev_num, 1);
printk("bye bye\n");
}
module_init(moduleparam_init);
module_exit(moduleparam_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
Makefile
obj-m += dev_t.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.orde \.*.cmd *mod
静态分配设备号:首先通过cat /proc/devices
查看哪些主设备号没有被占用的,比如220没有被占用,则通过sudo insmod dev_t.ko major=220 minor=0
[795650.456421] major is 220
[795650.456423] minor is 0
[795650.456424] register_chrdev_region succeed
[795677.933220] bye bye
动态分配设备号:直接通过sudo insmod dev_t.ko
[795552.620925] alloc_chrdev_region succeed
[795552.620927] major is 240
[795552.620927] minor is 0
[795632.868577] bye bye
Linux
中,使用cdev
结构体描述一个字符设备。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;
} __randomize_layout;
cdev_init
函数用于初始化cdev
结构体成员,建立cdev
和file_operations
之间的联系。函数原型如下(fs/char_dev.c
):
/**
* 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和file_operations之间的联系
cdev->ops = fops;
}
cdev_add
函数用于向系统添加一个cdev
结构体,也就是添加一个字符设备
/**
* 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)
{
int error;
p->dev = dev;
p->count = count;
if (WARN_ON(dev == WHITEOUT_DEV))
return -EBUSY;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
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);
}
cdev.c
#include
#include
#include
#include
#include
#include
dev_t dev_num;
struct cdev cdev_test;
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
};
static int modulecdev_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret)
{
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
cdev_test.owner = THIS_MODULE;
cdev_init(&cdev_test, &cdev_test_ops);
cdev_add(&cdev_test, dev_num, 1);
return 0;
}
static void modulecdev_exit(void)
{
unregister_chrdev_region(dev_num, 1);
cdev_del(&cdev_test);
printk("bye bye\n");
}
module_init(modulecdev_init);
module_exit(modulecdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
Makefile
obj-m += cdev.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.orde \.*.cmd *mod
Linux
有一个很重要的概念叫一切皆文件,也就是Linux
中的设备就像普通的文件一样。访问一个设备就好像是在访问一个文件。在应用程序中我们可以使用open,read,write,close,ioctl
这个几个系统调用来操作驱动。当我们在应用程序中调用open
函数的时候,最终会去执行驱动中的open
函数。所以file_operations
将系统调用和驱动程序连接起来了。
file_operations
结构体定义在include/linux/fs.h
文件当中,这个结构体非常的庞大。
struct file_operations {
struct module *owner;//拥有改结构体的模块指针,一般设置为THIS_MODULE
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 (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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); //与unlocked_ioctl功能一样
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 (*setfl)(struct file *, unsigned long);
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);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
本着Linux
中一切皆文件的思想。每个设备在Linux
系统中都有一个对应的"设备文件"代表他们,应用程序通过操作这个“设备文件”,便可以操作对应的硬件。如下代码所示:
fd = open("/dev/hello",O_RDWR);
这个“设备文件"就是设备节点。所以Linux
设备节点是应用程序和驱动程序沟通的一个桥梁。设备节点被创建在dev
目录下。
crw-rw-r--+ 1 root netdev 10, 242 Mar 19 18:55 /dev/rfkill
上述中rfkill
就是设备节点,位于/dev
目录下,c
表示他是一个字符设备,10
是主设备号,242
是次设备号。Linux
可以通过主设备号来找到他对应的file_operations
结构体。通过次设备号找到这个设备是同类设备中的第几个。这样就确定了是哪个驱动程序。
可以通过命令mknod
创建设备节点。
mknod
命令格式:
mknod 设备节点名称 设备类型(字符设备用c,块设备用b) 主设备号 次设备号
举例:
mknod /dev/test c 236 0
file.c
#include
#include
#include
#include
#include
#include
dev_t dev_num;
struct cdev cdev_test;
static int major = 0;
static int minor = 0;
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk("This is a cdev_test_read\n");
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk("This is a cdev_test_write\n");
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_release\n");
return 0;
}
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
static int file_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret)
{
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d\n", major);
printk("minor is %d\n", minor
cdev_test.owner = THIS_MODULE;
cdev_init(&cdev_test, &cdev_test_ops);
cdev_add(&cdev_test, dev_num, 1);
return 0;
}
static void file_exit(void)
{
unregister_chrdev_region(dev_num, 1);
cdev_del(&cdev_test);
printk("bye bye\n");
}
module_init(file_init);
module_exit(file_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
app.c(gcc app.c -o app.out)
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
char buf[64] = {};
fd = open("/dev/test", O_RDWR); // 打开设备节点
if (fd < 0)
{
perror("open error \n");
return fd;
}
close(fd);
return 0;
}
sudo insmod file.ko
[805697.526236] alloc_chrdev_region succeed
[805697.526237] major is 240
[805697.526237] minor is 0
sudo mknod /dev/test c 240 0
sudo ./app.out
[806101.252401] This is a cdev_test_open
[806101.252402] This is a cdev_test_release
可以通过mdev
机制实现设备节点的自动创建与删除
Linux
中可以通过udev
来实现设备节点的创建与删除,udev
是一个用户程序,可以根据系统中设备的状态来创建或者删除设备节点,比如当驱动程序成功加载到Linux
时会自动在/dev
目录下创建对应的设备节点,当驱动程序卸载的时候会自动删除/dev
目录下设备节点。
在嵌入式Linux
中我们使用的是mdev
,mdev
是udev
的简化版本。在使用busybox
构建根文件系统的时候,busybox
会自动创建mdev
。
class_create
函数定义在include/linux/device.h
文件当中,是一个宏定义,使用这个函数会在/sys/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); \
})
extern struct class * __must_check __class_create(struct module *owner,
const char *name,
struct lock_class_key *key);
class_create
一共有俩个参数,第一个参数owner
,一般为THIS_MODULE
,第个参数name
是类的名字。
使用class_create
创建好类以后,还需要使用device_create
函数在类下面创建一个设备。定义在include/linux/device.h
文件当中。如下所示:
struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...);
第一参数class
表示这个设备创建在哪个类下面,
第二个参数parent
是父设备,一般设置成NULL
,也就是没有父设备。
第三个参数dev_t
是设备号。第四个参数drvdata
是设备可能会用到的数据,设置成NULL
,
第五个参数fmt
是设备节点的名字。
使用device_destroy
函数可以删掉创建的设备,函数原型如下:
extern void device destroy(struct class *cls, dev t devt);
参数class
是要删除的设备所处的类,dev_t
是要删除的设备号
使用class_destroy
函数可以删掉创建的类,函数原型如下:
extern void class destroy(struct class *cls);
参数class
是要删除的类
file.c
#include
#include
#include
#include
#include
#include
#include
dev_t dev_num;
struct cdev cdev_test;
static int major = 0;
static int minor = 0;
struct class *class;
struct device *device;
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
printk("This is a cdev_test_read\n");
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
printk("This is a cdev_test_write\n");
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_release\n");
return 0;
}
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
static int file_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret)
{
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d\n", major);
printk("minor is %d\n", minor);
cdev_test.owner = THIS_MODULE;
cdev_init(&cdev_test, &cdev_test_ops);
cdev_add(&cdev_test, dev_num, 1);
class_create(THIS_MODULE, "test");
device_create(class, NULL, dev_num, NULL, "test");
return 0;
}
static void file_exit(void)
{
unregister_chrdev_region(dev_num, 1);
cdev_del(&cdev_test);
device_destroy(class, dev_num);
class_destroy(class);
printk("bye bye\n");
}
module_init(file_init);
module_exit(file_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
app.c(gcc app.c -o app.out)
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
char buf[64] = {};
fd = open("/dev/test", O_RDWR); // 打开设备节点
if (fd < 0)
{
perror("open error \n");
return fd;
}
close(fd);
return 0;
}
sudo insmod file.ko
[ 372.600726] alloc_chrdev_region succeed
[ 372.600728] major is 240
[ 372.600728] minor is 0
sudo ./app.out
fengzc@ubuntu:~/study/drivers_test/05_file$ ls -l /sys/class/test
total 0
lrwxrwxrwx. 1 root root 0 Mar 29 18:07 test -> …/…/devices/virtual/test/test
fengzc@ubuntu:~/study/drivers_test/05_file$ ls -l /dev/test
crw-------. 1 root root 240, 0 Mar 29 18:04 /dev/test
Linux
系统将可访问的内存空间分为了俩部分,一部分是内核空间,一部分是用户空间。操作系统和驱动程序运行在内核空间(内核态),应用程序运行在用户空间(用户态)。
为什么要区分内核空间和用户空间呢?
对硬件的资源的管理都是在内核空间中完成的,应用程序是无法直接对硬件进行操作的,我们只能通过调用内核的接口来完成这样的任务。比如应用程序要读取磁盘上的一个文件,应用程序可以向内核发起一个"系统调用"告诉内核:“我要读取磁盘上的文件”。这个过程其实就是通过一个特殊的指令让进程从用户态进入到内核态,在内核空间中,CPU
可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以继续往下执行了。
既然进程要从用户空间切换到内核空间才可以使用系统的硬件资源,切换方式有三种:系统调用,软中断,硬件中断。
内核空间和用户空间的内存是不能互相访问的。但是很多业务程序都需要和内核交互数据,比如应用程序使用read
函数从驱动中读取数据,使用write
函数向驱动中写数据。这就要需要借助copy_from_user
和copy_to_user
这两个函数完成数据传输。分别是将用户空间的数据拷贝到内核空间以及将内核空间的数据拷贝到用户空间。这俩个函数定义在了linux/include/asm-arm/uaccess.h
文件下。
copy_to_user
函数
函数原型:unsigned long copy_to_user(void_user *to, const void *from, unsigned long n);
作用:把内核空间的数据复制到用户空间
函数参数:*to
用户空间的指针。*from
内核空间的指针,n是从内核空间向用户空间拷贝的字节数。
函数返回值:成功返回0
copy_from_user
函数
函数原型:unsigned long copy_from_user(void * to, const void_user *from, unsigned long n)
作用:把用户空间的数据复制到内核空间
函数参数:*to
内核空间的指针。*from
用户空间的指针,n
是数据的长度
函数返回值:成功返回0
usr.c
#include
#include
#include
#include
#include
#include
#include
#include
dev_t dev_num;
struct cdev cdev_test;
static int major = 0;
static int minor = 0;
struct class *class;
struct device *device;
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
char kbuf[32] = "This is a cdev_test_read";
if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0)
{
printk("copy_to_user is error");
return -EFAULT;
}
printk("This is a cdev_test_read\n");
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
char kbuf[32] = {0};
if (copy_from_user(kbuf, buf, size) != 0)
{
printk("copy_from_user is error");
return -EFAULT;
}
printk("kbuf: %s\n", kbuf);
printk("This is a cdev_test_write\n");
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_release\n");
return 0;
}
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
static int usr_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret)
{
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
major = MAJOR(dev_num);
minor = MINOR(dev_num);
printk("major is %d\n", major);
printk("minor is %d\n", minor);
cdev_test.owner = THIS_MODULE;
cdev_init(&cdev_test, &cdev_test_ops);
cdev_add(&cdev_test, dev_num, 1);
class = class_create(THIS_MODULE, "test");
device_create(class, NULL, dev_num, NULL, "test");
return 0;
}
static void usr_exit(void)
{
unregister_chrdev_region(dev_num, 1);
cdev_del(&cdev_test);
device_destroy(class, dev_num);
class_destroy(class);
printk("bye bye\n");
}
module_init(usr_init);
module_exit(usr_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
app.c(gcc app.c -o app.out)
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
char buf1[32] = {};
char buf2[32] = {"nihao"};
fd = open("/dev/test", O_RDWR); // 打开设备节点
if (fd < 0)
{
perror("open error \n");
return fd;
}
read(fd, buf1, sizeof(buf1));
printf("buf1 is %s\n", buf1);
write(fd, buf2, sizeof(buf2));
return 0;
}
文件私有数据的概念在Linux
驱动中有着非常广泛的使用。文件私有数据就是将私有数据private_data
指向设备结构体。
然后在read
,write
等函数中通过private_data
访问设备结构体。
Linux
中没有明确的规定必须要使用文件私有数据,但是文件私有数据的使用在Linux
驱动中广泛使用,这是Linux
驱动遵循的“潜规则”。实际上也体现了Linux
面向对象的思想。
在Linux
中,使用主设备号来表示对应的某一类驱动。用次设备号表示这类驱动下的各个设备假如现在我们的驱动要支持主设备相同,但是次设备号不同的设备。我们的驱动要怎么写呢,这个就用到了的file
结构体中的私有数据private_date
。
函数原型:container_of(ptr, type, member)
函数功能:通过结构体变量中某个成员的首地址获取到整个结构体变量的首地址。
函数参数:第一个参数ptr
是结构体变量中某个成员的地址。第二个参数type
是结构体的类型。第三个参数member
是该结构体变量的具体名字。
usr.c
#include
#include
#include
#include
#include
#include
#include
#include
struct device_test
{
dev_t dev_num;
struct cdev cdev_test;
int major;
int minor;
struct class *class;
struct device *device;
char kbuf[32];
};
struct device_test dev1;
struct device_test dev2;
static int cdev_test_open(struct inode *inode, struct file *file)
{
dev1.minor = 0;
dev2.minor = 1;
file->private_data = container_of(inode->i_cdev, struct device_test, cdev_test);
printk("This is a cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *dev = (struct device_test *)file->private_data;
if (copy_to_user(buf, dev->kbuf, strlen(dev->kbuf)) != 0)
{
printk("copy_to_user is error");
return -EFAULT;
}
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *dev = (struct device_test *)file->private_data;
if (dev->minor == 0)
{
if (copy_from_user(dev->kbuf, buf, size) != 0)
{
printk("copy_from_user is error");
return -EFAULT;
}
printk("kbuf is %s\n", dev->kbuf);
}
else if (dev->minor == 1)
{
if (copy_from_user(dev->kbuf, buf, size) != 0)
{
printk("copy_from_user is error");
return -EFAULT;
}
printk("kbuf is %s\n", dev->kbuf);
}
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
return 0;
}
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
static int usr_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev1.dev_num, 0, 2, "alloc_name");
if (ret)
{
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
dev1.major = MAJOR(dev1.dev_num);
dev1.minor = MINOR(dev1.dev_num);
printk("major is %d\n", dev1.major);
printk("minor is %d\n", dev1.minor);
dev1.cdev_test.owner = THIS_MODULE;
cdev_init(&dev1.cdev_test, &cdev_test_ops);
cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
dev1.class = class_create(THIS_MODULE, "test1");
device_create(dev1.class, NULL, dev1.dev_num, NULL, "test1");
dev2.major = MAJOR(dev1.dev_num + 1);
dev2.minor = MINOR(dev1.dev_num + 1);
printk("major is %d\n", dev2.major);
printk("minor is %d\n", dev2.minor);
dev2.cdev_test.owner = THIS_MODULE;
cdev_init(&dev2.cdev_test, &cdev_test_ops);
cdev_add(&dev2.cdev_test, dev1.dev_num + 1, 1);
dev2.class = class_create(THIS_MODULE, "test2");
device_create(dev2.class, NULL, dev1.dev_num + 1, NULL, "test2");
return 0;
}
static void usr_exit(void)
{
unregister_chrdev_region(dev1.dev_num, 1);
unregister_chrdev_region(dev1.dev_num + 1, 1);
cdev_del(&dev1.cdev_test);
cdev_del(&dev2.cdev_test);
device_destroy(dev1.class, dev1.dev_num);
device_destroy(dev2.class, dev1.dev_num + 1);
class_destroy(dev1.class);
class_destroy(dev2.class);
printk("bye bye\n");
}
module_init(usr_init);
module_exit(usr_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
app.c(gcc app.c -o app.out)
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd1, fd2;
char buf1[32] = {"niaho /dev/test1"};
char buf2[32] = {"niaho /dev/test2"};
fd1 = open("/dev/test1", O_RDWR); // 打开设备节点
if (fd1 < 0)
{
perror("open error \n");
return fd1;
}
write(fd1, buf1, sizeof(buf1));
close(fd1);
fd2 = open("/dev/test2", O_RDWR); // 打开设备节点
if (fd2 < 0)
{
perror("open error \n");
return fd2;
}
write(fd2, buf2, sizeof(buf2));
close(fd2);
return 0;
}
sudo insmod file.ko
[ 217.022060] alloc_chrdev_region succeed
[ 217.022062] major is 240
[ 217.022062] minor is 0
[ 217.024609] major is 240
[ 217.024610] minor is 1
sudo ./app.out
fengzc@ubuntu:~/study/drivers_test/08_private_data_test$ ls -l /sys/class/test*
/sys/class/test1:
total 0
lrwxrwxrwx. 1 root root 0 Mar 29 22:03 test1 -> …/…/devices/virtual/test1/test1/sys/class/test2:
total 0
lrwxrwxrwx. 1 root root 0 Mar 29 22:03 test2 -> …/…/devices/virtual/test2/test2fengzc@ubuntu:~/study/drivers_test/08_private_data_test$ ls -l /dev/test*
crw-------. 1 root root 240, 0 Mar 29 22:02 /dev/test1
crw-------. 1 root root 240, 1 Mar 29 22:02 /dev/test2[ 271.300678] This is a cdev_test_open
[ 271.300681] kbuf is niaho /dev/test1
[ 271.300685] This is a cdev_test_open
[ 271.300686] kbuf is niaho /dev/test2
在Linux
中,把无法归类的五花八门的设备定义成杂项设备。相对与字符设备来说,杂项设备主设备固定为10,而字符设备不管是动态分配还是静态分配设备号,都会消耗一个主设备号,比较浪费主设备号。杂项设备会自己调用class_create()
和device_create()
来自动创建设备节点。所以可以把杂项设备看成是字符设备的一种。但是比我们平常写的字符设备降低了难度并节约了主设备号。
杂项设备使用结构体miscdevice
描述,定义在include/linux/miscdevice.h
文件当中。如下所示:
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
其中次设备号minor
一般使用宏MISC_DYNAMIC_MINOR
,表示自动分配次设备号。杂项设备主要依赖次设备号来管理不同的杂项设备。
注册杂项设备使用函数misc_register
函数,卸载杂项设备使用misc_deregister
函数。这俩个函数均定义在include\linux\miscdevice.h
文件当中。
函数原型:int misc_register(struct miscdevice *misc)
参数:杂项设备的结构体指针
返回值:成功返回0,失败返回负数。
函数原型:int misc_deregister(struct miscdevice *misc)
参数:杂项设备的结构体指针
返回值:成功返回0,失败返回负数。
usr.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
static int cdev_test_open(struct inode *inode, struct file *file)
{
printk("This is a cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
return 0;
}
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "test",
.fops = &cdev_test_ops,
};
static int usr_init(void)
{
int ret;
ret = misc_register(&misc_dev);
if (ret < 0)
{
printk("misc_register failed");
return -1;
}
return 0;
}
static void usr_exit(void)
{
misc_deregister(&misc_dev);
printk("bye bye\n");
}
module_init(usr_init);
module_exit(usr_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");
fengzc@ubuntu:~/study/drivers_test/09_misc$ ls -l /sys/class/misc/test
lrwxrwxrwx. 1 root root 0 Mar 29 22:25 /sys/class/misc/test -> …/…/devices/virtual/misc/test
fengzc@ubuntu:~/study/drivers_test/09_misc$ ls -l /dev/test
crw-------. 1 root root 10, 56 Mar 29 22:25 /dev/test
fengzc@ubuntu:~/study/drivers_test/09_misc$
在使用goto
语句处理错误时,要遵循“先进后出”的原则
内核中保留了地址0xffffffffff000~0xfffffffffff
(64位系统)用来记录错误码,这段地址和Linux
的错误码是一一对应的。内核基本错误码保存在errno-base.h
文件当中。
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H
#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */
#endif
内核中的函数常常返回指针,如果内核返回一个指针,那么就有三种情况:合法指针,NULL指针,非法指针。
如何判断函数返回的指针是有效地址还是错误码呢?使用IS_ERR
函数去检查函数的返回值,如果地址落在0xffffffffooo~oxfffffffffff
范围(64位系统),表示该函数执行失败,IS_ERR
为1,同时该函数返回的错误地址对应一个linux
的错误号。如果想知道这个地址是哪个错误码,就用PTR_ERR
函数来转化。其中IS_ERR
和PTR_ERR
函数定义在errno.h
当中。
示例:
if(IS_ERR(dev.device)){
ret = PTR_ERR(dev.device)
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct device_test
{
dev_t dev_num;
struct cdev cdev_test;
int major;
int minor;
struct class *class;
struct device *device;
char kbuf[32];
};
struct device_test dev1;
static int cdev_test_open(struct inode *inode, struct file *file)
{
file->private_data = &dev1;
printk("This is a cdev_test_open\n");
return 0;
}
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *dev = (struct device_test *)file->private_data;
if (copy_to_user(buf, dev->kbuf, strlen(dev->kbuf)) != 0)
{
printk("copy_to_user is error");
return -EFAULT;
}
return 0;
}
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *dev = (struct device_test *)file->private_data;
if (copy_from_user(dev->kbuf, buf, size) != 0)
{
printk("copy_from_user is error");
return -EFAULT;
}
printk("kbuf is %s\n", dev->kbuf);
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
return 0;
}
struct file_operations cdev_test_ops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
};
static int usr_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name");
if (ret < 0)
{
goto err_chrdev;
printk("alloc_chrdev_region failed\n");
}
printk("alloc_chrdev_region succeed\n");
dev1.major = MAJOR(dev1.dev_num);
dev1.minor = MINOR(dev1.dev_num);
printk("major is %d\n", dev1.major);
printk("minor is %d\n", dev1.minor);
dev1.cdev_test.owner = THIS_MODULE;
cdev_init(&dev1.cdev_test, &cdev_test_ops);
ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
if (ret < 0)
{
goto err_chr_add;
}
dev1.class = class_create(THIS_MODULE, "test");
if(IS_ERR(dev1.class)){
goto err_class_create;
}
device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
if(IS_ERR(dev1.device)){
goto err_class_device;
}
return 0;
err_class_device:
class_destroy(dev1.class);
err_class_create:
cdev_del(&dev1.cdev_test);
err_chr_add:
unregister_chrdev_region(dev1.dev_num, 1);
err_chrdev:
return ret;
}
static void usr_exit(void)
{
unregister_chrdev_region(dev1.dev_num, 1);
cdev_del(&dev1.cdev_test);
device_destroy(dev1.class, dev1.dev_num);
class_destroy(dev1.class);
printk("bye bye\n");
}
module_init(usr_init);
module_exit(usr_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fengzc");
MODULE_VERSION("v1.0");