Linux设备驱动程序(第三版)学习之字符驱动(二)

本文深入讲解了字符设备驱动的基本概念,包括主次设备号的作用、设备号的内部表示及分配方式,介绍了file_operations、file和inode等核心内核数据结构,并详细展示了字符设备的注册与注销过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. scull(Simple Character Utility for Loading Localities)

1). scull是一个字符驱动,操作一块内存区域好像它是一个设备.

2). scull不依赖于硬件,scull只是操作一些从内核分配的内存.

2. scull的设计

1). 编写驱动的第一步是定义驱动将要提供给用户程序的功能(机制)

3. 主次设备号

1). 字符设备通过文件系统中的名字来存取,这些名字称为文件系统的特殊文件,或者设备文件,或者文件系统的简单节点.通常位于/dev目录下.

2). 主设备号用来标示设备相连的驱动,如/dev/null和/dev/zero都是由驱动1来管理

crw-rw-rw- 1 root root 1, 3 Apr 11 2002 null

crw------- 1 root root 10, 1 Apr 11 2002 psaux
crw------- 1 root root 4, 1 Oct 28 03:04 tty1
crw-rw-rw- 1 root tty 4, 64 Apr 11 2002 ttys0
crw-rw---- 1 root uucp 4, 65 Apr 11 2002 ttyS1
crw--w---- 1 vcsa tty 7, 1 Apr 11 2002 vcs1
crw--w---- 1 vcsa tty 7,129 Apr 11 2002 vcsa1
crw-rw-rw- 1 root root 1, 5 Apr 11 2002 zero

3). 次设备号用来决定引用哪个设备

4). 依据你的驱动是如何编写的,你可以从内核得到一个你的设备的直接指针,或者可以自己使用次设备号作为本地设备数组的索引

4. 设备编号的内部表示

1). 设备号的类型是dev_t类型(包含在头文件<linux/types.h>),32位,高12位用作主设备号,低20位用作次设备号.

2). 在<linux/kdev_t.h>文件中定义了一套获取主次设备号的宏定义

MAJOR(dev_t dev);-------------------->获取主设备号

MINOR(dev_t dev);-------------------->获取次设备号

MKDEV(int major, int minor);-------------->将主次设备号合并成一个dev_t类型

5. 分配和释放字符设备编号

1). 在知道主设备号的情况下,也就是静态的分配了一个主设备号,来给驱动注册分配一个或多个次设备号的函数

int   register_chrdev_region(dev_t first_minor, unsigned  int  minor_count, char  *device_name);--------->在头文件<linux/fs.h>中声明

first_minor---------------------------->要分配的起始设备编号,通常是0

minor_count-------------------------->请求连续设备编号的总数

device_name------------------------->设备名,可以在/proc/devices和sysfs中看到.

分配成功: 返回0,否者返回个小于0的数

2). 动态分配一个主设备号的函数

int  alloc_chrdev_region(dev_t *dev, unsigned int first_minor, unsigned int minor_count, char *device_name);

dev------------------->一个出参,里面存放了分配的主设备号

3). 释放字符设备编号的函数

void  unregister_chrdev_region(dev_t first, unsigned int minor_count);   -------------->常用在模块退出时调用

6. 动态分配主设备号的缺点是:无法提前创建设备节点,(说白了,就是无法提前给设备起个名字,如tty0,tty1),因为主设备号是变化的.

在insmod模块后,一旦设备号分配了,就可以读取 /proc/devices来创建 设备文件.

7. 三个重要的内核数据结构--------------------->定义在<linux/fs.h>文件中

1). 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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, 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 (*dir_notify)(struct file *filp, unsigned long arg);
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 **);
}
;

a. struct module *owner

  第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载.乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 <linux/module.h> 中定义的宏.

b. loff_t (*llseek) (struct file *, loff_t, int);

llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.loff_t 参数是一个"long offset", 并且就算在 32 位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器

c. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).

d. ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);

初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL,所有的操作会由 read 代替进行(同步地).

e. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.

f. ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);

初始化设备上的一个异步写

g. int (*readdir) (struct file *, void *, filldir_t);

对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.

h. unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.

i. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的ioctl"), 系统调用返回一个错误.

j. int (*mmap) (struct file *, struct vm_area_struct *);

mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

k. int (*open) (struct inode *, struct file *);

尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.

l. int (*flush) (struct file *);

flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.


定义一个struct file_operations结构体变量并初始化一些驱动设备用到的函数

struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
};

########################################################################################################################

2). file

struct file {
/*
* fu_list becomes invalid after file_free is called and queued via
* fu_rcuhead for RCU freeing
*/
union {
struct list_headfu_list;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations*f_op;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
loff_t f_pos;
struct fown_structf_owner;
unsigned int f_uid, f_gid;
struct file_ra_statef_ra;


u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;


#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_headf_ep_links;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space*f_mapping;
#ifdef CONFIG_DEBUG_WRITECOUNT
unsigned long f_mnt_write_state;
#endif
};

a. file 和用户空间的FILE指针没有任何关系,FILE定义在C库中,不出现在内核代码中,struct file是一个内核结构,从不出现在用户程序中.

b. struct file代表了一个打开的文件.不是特定给驱动设备的,系统中每个打开的文件都有一个关联的struct file内核空间,当文件的所有实例都关闭后,内核就释放这个结构体.

c. mode_t f_mode;

文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可,但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.

d. loff_t f_pos;

当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ).驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它;读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.

e. unsigned int f_flags;

这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查O_NONBLOCK 标志来看是否是请求非阻塞操作; 其他标志很少使用. 特别地, 应当检查读/写许可, 使用f_mode 而不是 f_flags. 所有的标志在头文件 <linux/fcntl.h> 中定义.

f.

########################################################################################################################

3). inode

struct inode {
struct hlist_nodei_hash;
struct list_headi_list;
struct list_headi_sb_list;
struct list_headi_dentry;
unsigned long i_ino;
atomic_t i_count;
unsigned int i_nlink;
uid_t i_uid;
gid_t i_gid;
dev_t i_rdev;
u64 i_version;
loff_t i_size;
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t i_size_seqcount;
#endif
struct timespeci_atime;
struct timespeci_mtime;
struct timespeci_ctime;
unsigned int i_blkbits;
blkcnt_t i_blocks;
unsigned short          i_bytes;
umode_t i_mode;
spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
struct mutex i_mutex;
struct rw_semaphorei_alloc_sem;
const struct inode_operations*i_op;
const struct file_operations*i_fop;/* former ->i_op->default_file_ops */
struct super_block*i_sb;
struct file_lock*i_flock;
struct address_space*i_mapping;
struct address_spacei_data;
#ifdef CONFIG_QUOTA
struct dquot *i_dquot[MAXQUOTAS];
#endif
struct list_headi_devices;
union {
struct pipe_inode_info*i_pipe;
struct block_device*i_bdev;
struct cdev *i_cdev;
};
int i_cindex;


__u32 i_generation;


#ifdef CONFIG_DNOTIFY
unsigned long i_dnotify_mask; /* Directory notify events */
struct dnotify_struct*i_dnotify; /* for directory notifications */
#endif


#ifdef CONFIG_INOTIFY
struct list_headinotify_watches; /* watches on this inode */
struct mutex inotify_mutex; /* protects the watches list */
#endif


unsigned long i_state;
unsigned long dirtied_when; /* jiffies of first dirtying */


unsigned int i_flags;


atomic_t i_writecount;
#ifdef CONFIG_SECURITY
void *i_security;
#endif
void *i_private; /* fs or device private pointer */
};

a. inode结构由内核在内部用来表示文件,它和代表打开文件描述符的文件结构struct file是不同的,(也可以这么理解,就没有对文件进行任何的操作,就代表了"文件名"),可以对该"文件名"打开多次,但是所得到的"文件描述符"struct file是不一样的,但是都对应同一个"文件"inode.

b. 对于inode结构,对编写驱动用于的成员有两个:

dev_t   i_rdev;---------------------------->对于代表设备文件的节点,它包含了设备编号

struct cdev *i_cdev;---------------------->代表字符设备,

c. 通过inode获取主次设备号的函数宏

unsigned  int  imajor(struct inode  *inode);------------------>获得主设备号

unsigned  int  iminor(struct inode  *inode);------------------>获取次设备号

#################################################################################################################

8. 注册字符设备

struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};

a. 在内核中使用struct  cdev结构来代表字符设备---------------------->包含在<linux/cdev.h>头文件中

b. 在内核调用设备前,应该先将给  字符设备分配空间,和将设备添加到内核

c. 分配和初始化字符设备的两种方法

                1). 如果你想在运行时获得一个独立的cdev结构,初始化方法是:

struct  cdev  *my_cdev = cdev_alloc();

my_cdev->ops = &my_fops;------------------>指向 对字符设备的操作函数指针结构.

2). 将cdev结构嵌入到一个你自己设备特定的结构

void  cdev_init(struct  cdev  *cdev, struct  file_operations  *fops);------------>初始化已经分配好了的cdev结构

d. cdev建立好后,将它告诉内核

int  cdev_add(struct  cdev  *dev, dev_t  num,  unsigned  int  count);------------>将字符设备添加到内核

num------------------>这个设备响应的第一设备号

count---------------->关联到的设备号的设备的个数(常常为1)

e. 从系统中删除一个字符设备的函数

void  cdev_del(struct  cdev  *dev);

##############################################################################################################################

9. 注册字符设备的旧方法

int  register_chrdev(unsigned  int  major, const  char  *name, struct  file_operations  *fops);

major--------------------->主设备号

name--------------------->驱动的名字(在/proc/devices中可以找到)

a. 调用了该函数,会给给定所谓主设备号注册0-255的次设备号,并且为每个设备建立一个缺省的cdev结构.

b. 主次设备号不能大于255

10. 从系统删除设备的旧方法

int   unregister_chrdev(unsigned   int  major, const  char  *name);


/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr: the pointer to the member.
 * @type: the type of the container struct this is embedded in.
 * @member: the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr);\
(type *)( (char *)__mptr - offsetof(type,member) );})




##################################################################################################################

字符设备模块驱动的编写总结

1. 在模块的入口函数中主要做三件事情

1). 将设备号注册进内核

动态分配设备号的注册

alloc_chrdev_region(&devno, minor, count, name); ------------>minor一般为0

静态分配设备号的注册

cdevno = MKDEV(major, minor);

register_chrdev_region(cdevno, count, chrdev_name);

2). 为设备开辟空间和初始化

cdev_init(&mycdev, &fops);

3). 将设备添加进内核(将设备和设备号进行关联)

cdev_add(&mycdev, cdevno, count);

2. 在模块的退出函数中主要做两件事(当然也可以做些其他事情)

1). 撤销注册的设备号

unregister_chrdev_region(cdevno, count);

2). 删除添加到内核的设备

cdev_del(&mycdev);

###########################################################################################################################

驱动源代码

# include <linux/module.h>
# include <linux/init.h>
# include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
/**
  * The descriptions for the module
  **/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Bruce.Wang");
MODULE_DESCRIPTION("Test the char device");
MODULE_VERSION("1.0");
MODULE_ALIAS("CHRDEV");
//MODULE_DEVICE_TABLE("for device of char ");


/**
  *the major device number
  **/
static int major = 66;
static int minor = 88;
static int count = 2;
static dev_t cdevno;
static char chrdev_name[] = "Dediprog";


/**
  *char device
  **/
static struct cdev mycdev;


int my_open(struct inode *inodep, struct file *filep)
{
/**
 *Major and minor device numbers obtained by inode
 **/
int major = MAJOR(inodep->i_rdev);
int minor = MINOR(inodep->i_rdev);


printk("%s %d\n", __func__, __LINE__);
printk("major------>%d\nminor-------->%d\n",major, minor);
return 0;
}


size_t my_read(struct file *filep, char __user *buf, size_t count, loff_t *offset)
{
printk("%s, %d\n",__func__, __LINE__);
sprintf(buf,"from kernel space,Happy newer");
printk("to user space--------->%s, count: %d\n", buf, count);
return 10;
}




ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *off)
{
printk("%s %d\n", __func__, __LINE__);
printk("from user space--------->%s, count: %d\n", buf, count);
return 20;
}


loff_t my_llseek(struct file *filp, loff_t off, int cmd)
{
printk("%s %d\n", __func__, __LINE__);
printk("off: %#x, cmd: %d\n", off, cmd);
return 30;
}




int my_release(struct inode *inodp, struct file *filp)
{
printk("%s %d\n", __func__, __LINE__);
return 40;
}
/**
  *Operations for the character device
  **/
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
.release = my_release,
};


/**
  *The init function of the module
  **/
static int __init chrdev_init(void)
{
int ret;
/**
 *Combined major and minor device numbers
 **/
cdevno = MKDEV(major, minor);




/**
 *static register the number of the char device into the kernel
 **/
ret = register_chrdev_region(cdevno, count, chrdev_name);
if (ret < 0){
goto ERROR1;
}


/**
 *Allocate space for character devices and init it
 **/
cdev_init(&mycdev, &fops);


/**
 *Add the char device  into the kernel(that is,relate with device number)
 **/
ret = cdev_add(&mycdev, cdevno, count);
if (ret < 0){
goto ERROR2;
}
return 0;
ERROR1:
printk("register char device number failed!\n");
return ret;
ERROR2:
printk("relate char device with number failed!");
unregister_chrdev_region(cdevno, count);
return ret;
}




/**
  *Function executed when the module exits
  **/
static void __exit chrdev_exit(void)
{
/**
 *Cancellation the char device number from the kernel
 **/
unregister_chrdev_region(cdevno, count);


/**
 *Remove the char device from the kernel
 **/
cdev_del(&mycdev);
printk("See you later,guys!!\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);

#############################################################################################################################

驱动Makefile的编写

obj-m := test.o
all:
@make -C /lib/modules/2.6.38-8-generic/build M=$(shell pwd) modules
clean:
@rm -rf test.ko modules* *mod* test.o Module*

##########################################################################################################################

在用户层编写应用程序来访问自己的设备



从kernel层读取数据

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


int main(int argc, char *argv[])
{
int fd;
int ret;
char buf[128];


if(argc != 2){
fprintf(stderr, "Usage: %s dev_file\n", argv[0]);
return -1;
}


printf("(1) call open argv[1]=%s\n",argv[1]);
fd = open(argv[1], O_RDWR | O_NDELAY);


if(fd < 0){
perror("open");
return -2;
}


printf("(2) call read\n");
ret = read(fd, buf, 11);
printf("buf-------%s\nret = %d\n", buf,ret);
}



向kernel写数据

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


int main(int argc, char *argv[])
{
int fd;
int ret;
char buf[128];


if(argc != 2){
fprintf(stderr, "Usage: %s dev_file\n", argv[0]);
return -1;
}


printf("(1) call open argv[1]=%s\n",argv[1]);
fd = open(argv[1], O_RDWR | O_NDELAY);


if(fd < 0){
perror("open");
return -2;
}
sprintf(buf, "Bruce said Hello");
printf("(3) call write\n");
ret = write(fd, buf, 22);

}

###########################################################################################################

我所测试的均是在x86虚拟机的kernel直接将模块插入到PC主机系统了,

测试步骤:

编译驱动

[root@linux ~]# make

向内核中插入驱动模块

.........................# sudo insmod test.ko

查看模块是否插入,在打印的信息里找test

.........................# lsmod

查看内核打印的信息

.........................# dmesg

清空内核打印的信息

........................# sudo dmesg -c

####################################################################

在用户层执行可执行文件

创建一个名为Dediprog的字符设备名,仅仅是名字,方便用户层使用

[root@linux ~]# mknod Dediprog c 66 88

........................# sudo ./app Dediprog

从内核中移除模块的命令:

......................# sudo rmmod test -------------->注意不用加后缀ko






























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值