从内核中最简单的驱动程序入手,描述Linux驱动开发,主要文章目录如下(持续更新中):
01 - 第一个内核模块程序
02 - 注册字符设备驱动
03 - open & close 函数的应用
04 - read & write 函数的应用
05 - ioctl 的应用
06 - ioctl LED灯硬件分析
07 - ioctl 控制LED软件实现(寄存器操作)
08 - ioctl 控制LED软件实现(库函数操作)
09 - 注册字符设备的另一种方法(常用)
10 - 一个cdev实现对多个设备的支持
11 - 四个cdev控制四个LED设备
12 - 虚拟串口驱动
13 - I2C驱动
14 - SPI协议及驱动讲解
15 - SPI Linux驱动代码实现
16 - 非阻塞型I/O
17 - 阻塞型I/O
18 - I/O多路复用之 select
19 - I/O多路复用之 poll
20 - I/O多路复用之 epoll
21 - 异步通知
阻塞型IO相对于非阻塞型IO来说,最大的优点是资源不可用时进程主动放弃CPU让其他的进程运行,而不用不停的轮询,提高系统的效率,但是缺点也是比较明显的就是进程阻塞之后不能做其他的事情,这在一个进程要同时对多个设备进行操作时非常不便。解决这个问题的方法有很多,比如多进程、多线程和I/O多路复用,I/O复用有select、poll和Linux所持有的epoll三种方式,select、poll和epoll可以用于处理轮询,应用程序通过 select、epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动程序中编写 poll 函数。
本节首先说明select的用法。
应用层select函数的原型如下:
原 型: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功 能: 实现I/O多路复用
@param1: 要操作的文件描述符个数,通常被设置为select监听的所有文件描述符的最大值加1,因为文件描述符是从0开始的
最大为1024 #define __FD_SETSIZE 1024(include\uapi\linux\posix_types.h)
@param2: 指向文件描述符集合,用于监视指定描述符集的读变化,也就是监视这些文件是否可以读取,只要这些集合里面有一个文件可以读取那么 seclect 就会返回一个大于 0 的值表示文件可以读取。
如果没有文件可以读取,那么就会根据 timeout 参数来判断是否超时。可以将 readfs设置为 NULL,表示不关心任何文件的读变化。
@param3: 指向文件描述符集合,用于监视这些文件是否可以进行写操作
@param4: 指向文件描述符集合,用于监视这些文件的异常
@param5: 超时时间,当我们调用 select 函数等待某些文件描述符可以设置超时时间,超时时间使用结构体 timeval 表示
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微妙
}; // 当timeout为NULL的时候就表示无限期的等待。
@return: 0表示超时发生,但是没有任何文件描述符可以进行操作;-1发生错误;其他值表示可以进行操作的文件描述符个数。
在select函数中,readfds、writefds 和 exceptfds 这三个参数都是 fd_setfd_set类型的, fd_set变量常用操作如下:
void FD_ZERO(fd_set *set) // 将fd_set变量的所有位都清零
void FD_SET(int fd, fd_set *set) // 将fd_set变量的某个位置1,也就是向fd_set添加一个文件描述符,参数fd就是要加入的文件描述符
void FD_CLR(int fd, fd_set *set) // 将fd_set变量的某个位清零,也就是将一个文件描述符从fd_set中删除,参数fd就是要删除的文件描述符
int FD_ISSET(int fd, fd_set *set) // 用于测试fd_set的某个位是否置 1,也就是判断某个文件是否可以进行操作,参数fd就是要判断的文件描述符
注意:nfds通常被设置为select监听的所有文件描述符的最大值加1,因为文件描述符是从0开始的
当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问的时候,驱动程序file_operations 操作集中的 poll 函数就会执行。驱动中的poll函数原型如下:
原 型: unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);
@param1: 要打开的设备文件(文件描述符)
@param2: 结构体 poll_table_struct 类型指针,由应用程序传递进来的。一般将此参数传递给 poll_wait 函数
@return: 向应用程序返回设备或者资源状态,可以返回的资源状态如下:
POLLIN // 有数据可以读取
POLLPRI // 有紧急的数据需要读取
POLLOUT // 可以写数据
POLLERR // 指定的文件描述符发生错误
POLLHUP // 指定的文件描述符挂起
POLLNVAL // 无效的请求
POLLRDNORM // 等同于 POLLIN,普通数据可读
在驱动程序的 poll 函数中通常调用 poll_wait 函数将应用程序添加到poll_table中,poll_wait 函数不会引起阻塞,只是将应用程序添加到poll_table 中,poll_wait 函数原型如下:
原 型: void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
功 能: 将应用程序添加到poll_table中
@param1: 打开的设备文件
@param2: 要添加到poll_table中的等待队列头
@param3: poll_table指针,就是file_operations 中 poll 函数的 wait 参数
@return: 无返回值
demo.c是虚拟串口的驱动代码,在25和26行定义了读和写 的等待队列头,并在248和249行对读和写的等待队列头进行初始化。
在 vser_read 函数中如果FIFO是空的,并且以阻塞的方式打开的画,就将该进程休眠,同时在 vser_write 函数中对其进行唤醒(162行)。
在 vser_poll 函数中,将应用进程加入到 poll_table 中,如果 select 监听的文件描述符(虚拟串口设备)发生了事件(FIFO不为空)之后,会返回POLLIN表示设备可读,然后就可以在应用程序中对设备进行读操作。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define VSER_CHRDEV_ANME ("vser_chrdev")
#define VSER_CLASS_ANME ("vser_cls")
#define VSER_DEVICE_ANME ("vser_dev")
#define KFIFO_SIZE (16)
#define FLAG (0)
struct vser_dev{
dev_t dev_no;
int major;
int minor;
struct cdev cdev;
struct class *cls;
struct device *dev;
wait_queue_head_t rwqh; // 定义读的等待队列头
wait_queue_head_t wwqh; // 定义写的等待队列头
};
struct vser_dev test_vser_dev;
DEFINE_KFIFO(vser_fifo, char, KFIFO_SIZE); // 声明定义一个虚拟串口
static int vser_open(struct inode *inode, struct file *filp)
{
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
filp->private_data = &test_vser_dev;
return 0;
}
static int vser_release(struct inode *indoe, struct file *filp)
{
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
return 0;
}
static ssize_t vser_read(struct file *filp, char __user *userbuf, size_t size, loff_t *offset)
{
unsigned int copied_num, ret;
struct vser_dev *test_vser_dev = filp->private_data;
#if FLAG
DECLARE_WAITQUEUE(r_wait, current); // 定义读的等待队列节点
#endif
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
if ( kfifo_is_empty(&vser_fifo) ) // kfifo为空返回真
{
printk("kfifo_is_empty.\n");
if ( filp->f_flags & O_NONBLOCK ) // 如果非阻塞方式打开直接返回
{
printk("O_NONBLOCK.\n");
ret = -EAGAIN; // try again
}
#if FLAG
// 此段代码与84行代码意义相同,是76行代码的具体实现过程
// 如果使用此段代码,在本函数中err0中的代码段也需一起使用
add_wait_queue(&test_vser_dev->rwqh, &r_wait); // 将读的等待队列节点 添加到 读的等待队列头中
__set_current_state(TASK_INTERRUPTIBLE); // 改变进程状态为休眠
schedule(); // 调度其他进程执行
if ( signal_pending(current) ) // 被信号唤醒
{
ret = -ERESTARTSYS;
goto err0;
}
#endif
#if (~FLAG)
// condition 条件不成立的时候进程休眠,即kfifo为空时进程休眠
if ( wait_event_interruptible(test_vser_dev->rwqh, !kfifo_is_empty(&vser_fifo)) < 0 )
{
ret = -ERESTARTSYS;
}
#endif
goto err0;
}
printk("kfifo is not empty.\n");
if (size > KFIFO_SIZE)
{
size = KFIFO_SIZE; // 判断拷贝内容的大小
}
ret = kfifo_to_user(&vser_fifo, userbuf, size, &copied_num); // kfifo不为空将数据拷贝到用户空间
if (ret < 0)
{
printk("kfifo_to_user failed.\n");
ret = -EFAULT; // Bad Address
goto err0;
}
printk("%s copied_num = %d.\n", __FUNCTION__, copied_num);
if ( !kfifo_is_full(&vser_fifo) ) // kfifo不为满
{
wake_up_interruptible(&test_vser_dev->wwqh); // 唤醒写的等待队列头
}
return copied_num;
err0:
#if FLAG
set_current_state(TASK_RUNNING); // 设置当前进程为运行态
remove_wait_queue(&test_vser_dev->rwqh, &r_wait); // 将等待队列清除
#endif
return ret;
}
static ssize_t vser_write(struct file *filp, const char __user *userbuf, size_t size, loff_t *offset)
{
unsigned int copied_num = 0;
unsigned int ret = 0;
struct vser_dev *test_vser_dev = filp->private_data;
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
if ( kfifo_is_full(&vser_fifo) ) // kfifo为满返回真
{
printk("kfifo_is_full.\n");
if ( filp->f_flags & O_NONBLOCK ) // 判断是否以非阻塞方式打开
{
printk("%s -- O_NONBLOCK.\n", __FUNCTION__);
ret = -EAGAIN;
goto err0;
}
if (wait_event_interruptible(test_vser_dev->wwqh, !kfifo_is_full(&vser_fifo)) < 0)
{
ret = -ERESTARTSYS;
}
goto err0;
}
if (size > KFIFO_SIZE)
{
size = KFIFO_SIZE;
}
ret = kfifo_from_user(&vser_fifo, userbuf, size, &copied_num); // kfifo不为满,则将用户空间数据拷贝到内核空间
if (ret == -EFAULT)
{
printk("kfifo_from_user failed.\n");
goto err0;
}
printk("%s -- copied_num = %d.\n", __FUNCTION__, copied_num);
if ( !kfifo_is_empty(&vser_fifo) )
{
wake_up_interruptible(&test_vser_dev->rwqh); // 唤醒读的等待队列
}
return copied_num;
err0:
return ret;
}
unsigned int vser_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct vser_dev *test_vser_dev = filp->private_data;
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
// 将当前进程加入到等待队列中,但并不阻塞
poll_wait(filp, &test_vser_dev->rwqh, wait);
if ( !kfifo_is_empty(&vser_fifo) ) // FIFO不为空
{
mask |= POLLIN | POLLRDNORM;
}
return mask;
}
struct file_operations vser_fops =
{
.open = vser_open,
.release = vser_release,
.read = vser_read,
.write = vser_write,
.poll = vser_poll,
};
static int __init vser_init(void)
{
int ret;
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
if (test_vser_dev.major)
{
test_vser_dev.dev_no = MKDEV(test_vser_dev.major, 0);
ret = register_chrdev_region(test_vser_dev.dev_no, 1, VSER_CHRDEV_ANME);
if (ret < 0)
{
printk("register_chrdev_region failed.\n");
goto register_chrdev_region_err;
}
}
else
{
ret = alloc_chrdev_region(&test_vser_dev.dev_no, 0, 1, VSER_CHRDEV_ANME);
if (ret < 0)
{
printk("alloc_chrdev_region failed.\n");
goto alloc_chrdev_region_err;
}
}
cdev_init(&test_vser_dev.cdev, &vser_fops);
ret = cdev_add(&test_vser_dev.cdev, test_vser_dev.dev_no, 1);
if (ret < 0)
{
printk("cdev_add failed.\n");
goto cdev_add_err;
}
test_vser_dev.cls = class_create(THIS_MODULE, VSER_CLASS_ANME);
if ( IS_ERR(test_vser_dev.cls) )
{
printk("class_create failed.\n");
ret = PTR_ERR(test_vser_dev.cls);
goto class_create_err;
}
test_vser_dev.dev = device_create(test_vser_dev.cls, NULL, test_vser_dev.dev_no, NULL, VSER_DEVICE_ANME);
if ( IS_ERR(test_vser_dev.dev) )
{
printk("device_create failed.\n");
ret = PTR_ERR(test_vser_dev.dev);
goto device_create_err;
}
init_waitqueue_head(&test_vser_dev.rwqh); // 初始化读的等待队列头
init_waitqueue_head(&test_vser_dev.wwqh); // 初始化写的等待队列头
return 0;
device_create_err:
class_destroy(test_vser_dev.cls);
class_create_err:
cdev_del(&test_vser_dev.cdev);
cdev_add_err:
unregister_chrdev(test_vser_dev.major, VSER_CHRDEV_ANME);
alloc_chrdev_region_err:
register_chrdev_region_err:
return ret;
}
static void __exit vser_exit(void)
{
printk("%s -- %d.\n", __FUNCTION__, __LINE__);
device_destroy(test_vser_dev.cls, test_vser_dev.dev_no);
class_destroy(test_vser_dev.cls);
cdev_del(&test_vser_dev.cdev);
unregister_chrdev(test_vser_dev.major, VSER_CHRDEV_ANME);
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
在 test.c 中首先打开了虚拟串口设备和触摸屏设备(打开触摸屏设备是为了显示当需要监听多个文件描述符时是如何编程的),本实例仅监听读操作,所以在52行的 select 函数中将参数3和参数4设置为NULL,如果监听的设备产生了事件,分别在代码的56行和73行使用FD_ISSET函数来判断是哪个设备产生的事件,进行相应的操作。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_NUM (2)
const char *dev_pathname = "/dev/vser_dev";
int main(int argc, char *argv[])
{
int fd[DEVICE_NUM], ret, i; // 要监视的文件描述符
fd_set readfds; // 读操作文件描述符集
struct timeval timeout; // 超时结构体
char r_buf[16];
char w_buf[16] = "hello select";
// 打开自己定义的设备
fd[0] = open(dev_pathname, O_RDWR | O_NONBLOCK, 0666);
if (fd[0] < 0)
{
perror("open");
return -1;
}
printf("my fd = %d\n", fd[0]);
// 打开触摸屏设备
fd[1] = open("/dev/input/touchscreen0", O_RDWR | O_NONBLOCK);
if(fd[1] < 0)
{
perror("open");
return -1;
}
printf("touch screen fd = %d\n", fd[1]);
while(1)
{
FD_ZERO(&readfds); // 清除 readfds
for (i=0; i<DEVICE_NUM; i++)
{
FD_SET(fd[i], &readfds);// 将 fd 添加到 readfds 里面
}
timeout.tv_sec = 10; // 5s
timeout.tv_usec = 1000; // 1000us
ret = select(fd[1]+1, &readfds, NULL, NULL, &timeout);
if (ret > 0)
{
if ( FD_ISSET(fd[0], &readfds) == 1 )
{
ret = read(fd[0], r_buf, sizeof(r_buf));
if (ret > 0)
{
printf("r_buf = %s\n", r_buf);
}
else if (ret == 0)
{
printf("read end of file\n");
}
else
{
perror("read");
}
}
if ( FD_ISSET(fd[1], &readfds) == 1 )
{
printf("touch screen\n");
}
}
else if (ret == 0)
{
printf("timeout\n");
}
else
{
printf("select error\n");
}
}
for(i=0; i<DEVICE_NUM; i++)
{
printf("close\n");
close(fd[i]);
}
return 0;
}
KERNELDIR ?= /home/linux/ti-processor-sdk-linux-am335x-evm-04.00.00.04/board-support/linux-4.9.28/
PWD := $(shell pwd)
EXEC = app
OBJS = test.o
CC = arm-linux-gnueabihf-gcc
$(EXEC):$(OBJS)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) modules
$(CC) $^ -o $@
.o:.c
$(CC) -c $<
install:
sudo cp *.ko app /tftpboot
# sudo cp *.ko app /media/linux/rootfs1/home/root/
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) clean
rm app
# sudo ls -l /media/linux/rootfs1/home/root/
clean:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) clean
rm app
obj-m += demo.o
测试结果的分析在结果中以注释的方式表示出来。
在结果中18行和32行打印了vser_release – 43. 不明白为什么???希望看到的童鞋了解到可以解答一下,谢谢
root@am335x-evm:~# insmod demo.ko //加载模块
[ 150.837233] vser_init -- 202.
root@am335x-evm:~# ./app & // 后台运行应用程序
[1] 883
root@am335x-evm:~# [ 158.450645] vser_open -- 34.
my fd = 3 // 打开自己创建的设备的文件描述符编号
touch screen fd = 4 // 打开触摸屏设备的文件描述符编号
[ 168.467064] vser_poll -- 176. // 调用poll函数
timeout // 10s多内没有任何操作打印超时
[ 168.470520] vser_poll -- 176.
// 按下回车键,显示命令行向FIFO中写数据
root@am335x-evm:~# echo "123" > /dev/vser_dev
[ 171.181425] vser_open -- 34.
[ 171.184807] vser_write -- 128. // 调用write函数
[ 171.193848] vser_write -- copied_num = 4.
[ 171.203131] vser_poll -- 176. // 再次调用poll函数,发现FIFO不为空,返回POLLIN,可以进行读操作
[ 171.206314] vser_read -- 57. // 调用读函数
root@am335x-evm:~# [ 171.217054] vser_release -- 43. // 不明白为什么会打印这样一句???在哪里调用了
[ 171.231758] kfifo is not empty. // FIFO不为空
[ 171.234962] vser_read copied_num = 4. // 将内核空间数据拷贝给用户空间
r_buf = 123 // 打印拷贝出的数据
[ 171.247214] vser_poll -- 176.
[ 181.258261] vser_poll -- 176.
timeout // 超时
[ 181.261638] vser_poll -- 176.
// 按在回车键,再次向虚拟串口设备写数据
root@am335x-evm:~# echo "789987" > /dev/vser_dev
[ 187.293996] vser_open -- 34.
[ 187.297366] vser_write -- 128.
[ 187.300501] vser_write -- copied_num = 7.
[ 187.304790] vser_poll -- 176.
root@am335x-evm:~# [ 187.312917] vser_release -- 43. // ???
[ 187.326221] vser_read -- 57.
[ 187.333348] kfifo is not empty.
[ 187.336561] vser_read copied_num = 7.
r_buf = 789987 // 将写入的数据读出来并打印
[ 187.350933] vser_poll -- 176.
root@am335x-evm:~# ps -aux // 查看当前运行的进程
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 2.7 1.5 5232 3936 ? Ss 20:20 0:05 /sbin/init
root 2 0.0 0.0 0 0 ? S 20:20 0:00 [kthreadd]
... ...
root 883 0.2 0.3 1340 792 ttyS0 S 20:22 0:00 ./app
root 889 0.8 0.4 1812 1216 ? Ss 20:23 0:00 /sbin/agetty -8
root 890 0.0 0.5 2636 1360 ttyS0 R+ 20:23 0:00 ps -aux
root@am335x-evm:~# [ 197.361980] vser_poll -- 176.
timeout
[ 197.365379] vser_poll -- 176.
root@am335x-evm:~# kill -9 883 // 将./app进程杀死
[ 204.182275] vser_poll -- 176.
root@am335x-evm:~# [ 204.216821] vser_release -- 43.
[1]+ Killed ./app
root@am335x-evm:~# rmmod demo.ko // 卸载模块
[ 209.330609] vser_exit -- 266.