從內核中最簡單的驅動程序入手,描述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 - 異步通知
文章目錄
1. IO多路複用簡介
阻塞型IO相對於非阻塞型IO來說,最大的優點是資源不可用時進程主動放棄CPU讓其他的進程運行,而不用不停的輪詢,提高系統的效率,但是缺點也是比較明顯的就是進程阻塞之後不能做其他的事情,這在一個進程要同時對多個設備進行操作時非常不便。解決這個問題的方法有很多,比如多進程、多線程和I/O多路複用,I/O複用有select、poll和Linux所持有的epoll三種方式,select、poll和epoll可以用於處理輪詢,應用程序通過 select、epoll 或 poll 函數來查詢設備是否可以操作,如果可以操作的話就從設備讀取或者向設備寫入數據。當應用程序調用 select、epoll 或 poll 函數的時候設備驅動程序中的 poll 函數就會執行,因此需要在設備驅動程序中編寫 poll 函數。
2. select相關函數
本節首先說明select的用法。
2.1 應用層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開始的
2.2 驅動中的函數接口
當應用程序調用 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: 無返回值
3. 示例代碼
3.1 demo.c
demo.c是虛擬串口的驅動代碼,在25和26行定義了讀和寫 的等待隊列頭,並在248和249行對讀和寫的等待隊列頭進行初始化。
在 vser_read 函數中如果FIFO是空的,並且以阻塞的方式打開的畫,就將該進程休眠,同時在 vser_write 函數中對其進行喚醒(162行)。
在 vser_poll 函數中,將應用進程加入到 poll_table 中,如果 select 監聽的文件描述符(虛擬串口設備)發生了事件(FIFO不爲空)之後,會返回POLLIN表示設備可讀,然後就可以在應用程序中對設備進行讀操作。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/kfifo.h>
#include <linux/wait.h>
#include <linux/poll.h>
#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");
3.2 test.c
在 test.c 中首先打開了虛擬串口設備和觸摸屏設備(打開觸摸屏設備是爲了顯示當需要監聽多個文件描述符時是如何編程的),本實例僅監聽讀操作,所以在52行的 select 函數中將參數3和參數4設置爲NULL,如果監聽的設備產生了事件,分別在代碼的56行和73行使用FD_ISSET函數來判斷是哪個設備產生的事件,進行相應的操作。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#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;
}
3.2 Makefile
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
3.4 測試結果
測試結果的分析在結果中以註釋的方式表示出來。
在結果中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.