17-阻塞型IO

從內核中最簡單的驅動程序入手,描述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,當以非阻塞的方式打開設備時如果資源不可用,則會立即返回 EAGAIN 並嘗試重新獲取資源,非阻塞性IO會定期嘗試查看資源是否可以獲取,這種方式效率較低。本文來講述另外一種IO操作——阻塞性IO,阻塞型IO的執行過程如下所示:
在這裏插入圖片描述
 當進程以阻塞的方式打開設備時,如果進程發現資源不可用會主動將自己的狀態設置爲 TASK_UNINTERRUPTIBLE 或 TASK_INTERRUPTIBLE 然後將自己加入一個驅動所維護的等待隊列中,最後調用 sehedule 主動放棄CPU,操作系統將其從運行隊列上移除,並調度其他的進程執行

2. 阻塞型IO的定義及相關函數

2.1 結構體定義

 阻塞訪問最大的好處就是當設備文件不可操作的時候進程可以進入休眠態,這樣可以將CPU 資源讓出來。阻塞性IO也有其缺點即進程在休眠的時間就不能做其他的事情。進入休眠狀態後當設備文件可以操作的時候就必須喚醒進程,一般在中斷函數裏面完成喚醒工作。Linux 內核提供了等待隊列(wait queue)來實現阻塞進程的喚醒工作,要實現等待隊列,內核中定義瞭如下的結構體來實現(include\linux\wait.h)

struct __wait_queue_head {
	spinlock_t lock;
	struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;	// 等待隊列頭


struct __wait_queue {
	unsigned int flags;
	void *private;
	wait_queue_func_t func;
	struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;	// 等待隊列節點

 其中 wait_queue_head_t 表示等待隊列頭即一個等待隊列的頭部,每個訪問設備的進程都是一個隊列項用 wait_queue_t 表示,當設備不可用的時候就要將這些進程對應的等待隊列項添加到等待隊列裏面。

2.2 初始化函數

 定義初始化等待隊列頭

DECLARE_WAIT_QUEUE_HEAD(name)	// 靜態定義一個等待隊列頭
init_waitqueue_head(q)			// 初始化一個等待隊列頭

 定義初始化等待隊列節點(項)

@原   型: DECLARE_WAITQUEUE(name, tsk)
@功   能: 給當前正在運行的進程創建並初始化了一個等待隊列項
@param1: 等待隊列項的名字
@param2: 這個等待隊列項屬於哪個任務(進程),一般設置爲current,在Linux內核中current相當於一個全局變量,表示當前進程 

2.3 添加/刪除隊列節點函數

 添加隊列節點

@原   型: void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
@功   能: 將隊列項添加至等待隊列頭的隊列中 
@param1: 要加入的等待隊列的等待隊列頭
@param2: 要加入的等待隊列項
@return: 無返回值

 刪除隊列節點

@原   型: void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
@功   能: 從等待隊列中移除等待隊列項
@param1: 要操作的等待隊列頭
@param2: 要移除的等待隊列項
@return: 無返回值

2.4 喚醒操作

@原   型: wake_up(wait_queue_head_t *q)
@功   能: 將這個等待隊列頭中的所有進程都喚醒,可以喚醒處於 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 狀態的進程
@param1: 要喚醒的等待隊列頭
@return: 無返回值


@原   型: wake_up_interruptible(wait_queue_head_t *q)
@功   能: 將這個等待隊列頭中的所有進程都喚醒,只能喚醒處於 TASK_INTERRUPTIBLE 狀態的進程
@param1: 要喚醒的等待隊列頭
@return: 無返回值


@原   型: wake_up_locked(wait_queue_head_t *q)
@功   能: 將這個等待隊列頭中的所有進程都喚醒
@param1: 要喚醒的等待隊列頭
@return: 無返回值

2.5 等待事件

 除了主動喚醒以外,也可以設置等待隊列等待某個事件,當這個事件滿足以後就自動喚醒等待隊列中的進程,相關函數定義如下

@原   型: wait_event(wq, condition)
@功   能: 條件 condition 不成立,將當前進程放入到等待隊列並休眠,此函數會將進程設置爲TASK_UNINTERRUPTIBLE 狀態
@param1: 等待隊列頭
@param2: 觸發條件

@原   型: wait_event_timeout(wq, condition, timeout)
@功   能: 功能和 wait_event 類似,但是此函數可以添加超時時間,以 jiffies 爲單位
@param1: 等待隊列頭
@param2: 觸發條件
@param3: 超時時間
@return: 0表示超時時間到,且condition爲假。1表示condition爲真,也就是條件滿足了

wait_event_interruptible(wq, condition)						// 進程休眠時可以通過信號來喚醒
wait_event_interruptible_timeout(wq, condition, timeout)	// 
wait_event_interruptible_exclusive(wq, condition)			// 排他性,默認情況下喚醒操作會把隊列中所有進程都喚醒,如果一個進程以排他方式休眠則喚醒時不會喚醒其他進程
wait_event_interruptible_locked(wq, condition)				// 調用前先獲得等待隊列內部的鎖
wait_event_interruptible_lock_irq(wq, condition, lock)		// 上鎖的同時禁止中斷

返回值: 不帶 timeout 返回0表示被成功喚醒,返回 -ERESTARTSYS 表示被信號喚醒
	    帶 timeout 返回0表示超時,返回大於0表示被成功喚醒,這個值表示離超時還剩餘的時間

等待事件和喚醒操作對應的關係爲:
   帶 locked 的用 wake_up_locked 喚醒
   不帶 locked 帶 interruptible 的用 wake_up_interruptible 喚醒
   否則用 wake_up 喚醒

2.6 其他函數

@原   型: int signal_pending(struct task_struct *p)
		  int signal_pending(current)
@功   能: 檢查當前進程是否有信號處理,返回不爲0表示有信號需要處理。
@return:  返回 -ERESTARTSYS 表示信號函數處理完畢後重新執行信號函數前的某個系統調用。也就是說,如果信號函數前有發生系統調用,在調度信號處理函數之前,內核會檢查系統調用的返回值,看看是不是因爲這個信號而中斷了系統調用。如果返回值-ERESTARTSYS,並且當前調度的信號具備-ERESTARTSYS屬性,系統就會在用戶信號函數返回之後再執行該系統調用。


@原   型: __set_current_state(state_value)
@功   能:  改變進程狀態
@param1: TASK_RUNNING			// 可運行狀態
		 TASK_INTERRUPTIBLE		// 可中斷的等待狀態
		 TASK_UNINTERRUPTIBLE 	// 不可中斷的等待狀態


@原   型: void schedule(void)		 
@功   能: 進行一次任務切換,調度其他進程執行
@return: 無返回值

3. 示例代碼

 本例中繼續使用虛擬串口驅動來實現阻塞型I/O,上一節當fifo中的數據爲空時,直接return,本例中實現的功能是讀操作時如果fifo中的數據爲空,則讀進程休眠,此時FIFO不爲滿喚醒寫進程;寫操作時FIFO不爲滿,將用戶空間數據拷貝到內核空間,喚醒讀進程。
 具體實現代碼如下:

3.1 demo.c

#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>

#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	
		// 此段代碼與83行代碼意義相同,是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;
}

struct file_operations vser_fops = 
{
	.open = vser_open,
	.release = vser_release,
	.read = vser_read,
	.write = vser_write,
};

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

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

int main(int argc, char *argv[])
{
	int fd, ret;
	char cmd;
	const char *dev_pathname = "/dev/vser_dev";
	char read_buf[8]  = "\0";
	char write_buf[8] = "abc"; 

	if (argc < 2)
	{
		printf("please input param 'w' or 'r'\n");
		return -1;
	}
	
	fd = open(dev_pathname, O_RDWR , 0666);	// 默認以阻塞方式打開設備文件
	if (fd < 0)
	{
		perror("open");
		return -1;
	}

	cmd = argv[1][0];
	switch(cmd)
	{
	case 'r':
		ret = read(fd, read_buf, sizeof(read_buf));
		if (ret == -1)	// 讀失敗
		{
			perror("read");
		}
		else if (ret == 0)
		{
			printf("read end of file\n");
		}
		else
		{
			printf("read_buf = %s\n", read_buf);
		}
		break;
	case 'w':
		ret = write(fd, write_buf, strlen(write_buf));
		if (ret == -1)
		{
			perror("write");
		}
		break;
	default:
		printf("cmd error\n");
	}

	close(fd);
	
	return 0;
}

3.3 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 測試結果

root@am335x-evm:~# insmod demo.ko 
[  445.778808] vser_init -- 182.
root@am335x-evm:~# ./app r &
[1] 990
[  449.686864] vser_open -- 33.
[  449.689856] vser_read -- 56.
[  449.698447] kfifo_is_empty.
root@am335x-evm:~# ./app w
[  452.273161] vser_open -- 33.
[  452.276157] vser_write -- 127.
[  452.279248] vser_write -- copied_num = 3.
read_buf = abc
[  456.627227] vser_release -- 41.
[1]+  Done                    ./app r
[  456.841265] vser_release -- 41.
root@am335x-evm:~# rmmod demo.ko 
[  508.718326] vser_exit -- 246.
發佈了57 篇原創文章 · 獲贊 65 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章