Linux 内核学习(8) --- 字符设备操作函数
目录
- file_operation 接口实现
- open close接口的实现
- read 接口实现
- write 接口实现
- ioctl 接口实现
- ioctl cmd 值的定义
- 如何检查命令
- 内存拷贝
- 字符设备注册函数
- cdev 操作相关函数
file_operation 接口实现
open close接口的实现
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
read 接口实现
ssize_t (*read) (struct file * filp, char __user *buf, size_t count, loff_t * f_pos);
struct file * filp:打开设备节点时分配的 struct file
类型
char __user * buf:待写入所读取数据的用户空间缓冲区指针
size_t count:待读取数据字节数
loff_t f_pos:待读取数据文件位置,读取完成后根据实际读取字节数重新定位
__user
:是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址
如果该操作为空,将使得 read
系统调用返回负EINVAL
失败,正常返回实际读取的字节数
write 接口实现
ssize_t (*write) (struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
struct file *filp:待操作的设备文件 struct file
结构体指针
const char __user buf:待写入所读取数据的用户空间缓冲区指针
size_t count:待写入数据字节数
loff_t * f_pos:待写入数据文件位置,读取完成后根据实际读取字节数重新定位
ioctl 接口实现
long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg)
long (*compat_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);
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
应用层调用 ioctl
的方式
int ioctl(int fd, int cmd, ...);
//参数:
//fd:打开设备文件的时候获得文件描述符
//cmd:第二个参数:给驱动层传递的命令,需要注意的时候,驱动层的命令和应用层的命令一定要统一
//第三个参数: "..."在C语言中,很多时候都被理解成可变参数。
当我们通过 ioctl
调用驱动层 xxx_ioctl
的时候,有三种情况可供选择:
- 不传递数据给
ioctl
- 传递数据给
ioctl
,希望它最终能把数据写入设备(例如:设置Framebuffer
的显示信息) - 调用
_ioctl
希望获取设备的硬件参数(例如:获取当前串口设备的波特率)
这三种情况中,有时候需要从用户空间读取数据,有时候需要从内核空间拷贝数据,有时候不需要传递数据,
用 “…” 来表示,可以带一个参数,或者不带参数
ioctl cmd 值的定义
include/uapi/asm-generic/ioctl.h
#define _IOC(dir,type,nr,size) \(((dir) << _IOC_DIRSHIFT) | \ //30((type) << _IOC_TYPESHIFT) | \ //8((nr) << _IOC_NRSHIFT) | \ //0((size) << _IOC_SIZESHIFT)) //16#ifndef __KERNEL__
#define _IOC_TYPECHECK(t) (sizeof(t))
#endif/* used to create numbers */
#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOR_BAD(type,nr,size) _IOC(_IOC_READ,(type),(nr),sizeof(size))
#define _IOW_BAD(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
#define _IOWR_BAD(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))/* used to decode ioctl numbers.. */
#define _IOC_DIR(nr) (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr) (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr) (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr) (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)
type:一般为 magincNumber
nr:一般为递增的序列号
size:传输数据的size
cmd 是一个 unsigned int
类型的整形数,大小为 32bit
cmd
的高位 2Bit cmd[31:30] 是数据的传输方向,可以是_IOC_READ
,_IOC_WRITE
或者IOC_READ|_IOC_WRITE
和_IOC_NONE
cmd[29:16]
最多是 14 bit,表示数据的大小cmd[15:8]
命令的类型,一般使用一个固定的数值,称为MagicNumber
cmd[7:0]
序列号,一般表示第几个命令,随着命令数递增
通过 linux
中提供的宏,我们只需要定义 MagicNumber
,命令序号和数据字段(宏中会自动使用sizeof)就好了
如何检查命令
_IOC_TYPE(nr)
来判断应用程序传下来的命令type是否正确
_IOC_DIR(nr)
来得到命令是读还是写,然后再通过宏 access_ok(type,addr,size)
来判断用户层传递的内存地址是否合法
内存拷贝
include/asm-generic/uaccess.h
static inline int copy_from_user(void *to, const void __user volatile *from,unsigned long n)
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)/* to:目标地址(用户空间)from:源地址(内核空间)n:将要拷贝数据的字节数*/#define put_user(x, ptr) \
({ \void *__p = (ptr); \might_fault(); \access_ok(VERIFY_WRITE, __p, sizeof(*ptr)) ? \__put_user((x), ((__typeof__(*(ptr)) *)__p)) : \-EFAULT; \
})
/*data:可以是字节、半字、字、双字类型的内核变量ptr:用户空间内存指针
*/#define get_user(x, ptr) \
({ \const void *__p = (ptr); \might_fault(); \access_ok(VERIFY_READ, __p, sizeof(*ptr)) ? \__get_user((x), (__typeof__(*(ptr)) *)__p) : \((x) = (__typeof__(*(ptr)))0,-EFAULT); \
})
/*data:可以是字节、半字、字、双字类型的内核变量ptr:用户空间内存指针
*/
Linux 提供 copy_from_user,copy_to_user,put_user 和 get_user宏来和用户空间交换数据
使用这些函数和用户空间交互时,内核会做参数检查,比如指针指向的区域是否属于用户空间,是否属于用户的当前进程,读和写的内存必须由相应的权限
字符设备注册函数
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()
、alloc_chrdev_region()
和 register_chrdev()
代码位置:
include/linux/fs.h kernel/fs/char_dev.c
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}static inline void unregister_chrdev(unsigned int major, const char *name)
{__unregister_chrdev(major, 0, 256, name);
}int register_chrdev_region(dev_t from, unsigned count, const char *name)int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)int unregister_chrdev_region(dev_t, unsigned);
register_chrdev
可以直接传入 file_opeations
结构体,本质上相当于将 cdev
的操作在函数内部实现了
register_chrdev_region
可以指定主设备号,但需要配合 cdev
结构体一起使用
alloc_chrdev_region
动态分配主设备号,传出 dev_t
的结构
register_chrdev_region(dev_t first,unsigned int count,char *name)
first
: 主设备号,要分配的设备编号范围的初始值, 这组连续设备号的起始设备号
count
: 连续编号范围. 是这组设备号的大小(也是次设备号的个数)
name
: 编号相关联的设备名称. (/proc/devices);本组设备的驱动名
正确的时候返回 0
register_chrdev_region
需要和 cdev
的相关函数一起使用
int alloc_chrdev_region(dev_t* dev ,unsigned int first minor,unsigned int count,char *name)
dev_t* dev
是传出的参数 用于获取 dev_t
数据结构 自动获取主次设备号
first_minor
第一个次设备号
count
请求连续分配的设备个数
name
出现在 /proc/devices 和 sysfs 中
cdev 操作相关函数
include/linux/cdev.h
用于分配,初始化一个cdev 结构,将struct file_operations赋值给它,最后将它和 dev_t
结构绑定
struct cdev {struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);void cdev_put(struct cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);void cdev_set_parent(struct cdev *p, struct kobject *kobj);
int cdev_device_add(struct cdev *cdev, struct device *dev);
一般用法如下:
demo_cdev = cdev_alloc();
cdev_init(demo_cdev,&Mstar_demo_driver);
demo_cdev->owner = THIS_MODULE; ret = cdev_add(demo_cdev,demo_devt,1);
if(ret)
{ printk("cdev create error!\n"); unregister_chrdev_region(demo_devt,1); return ret;
}