目錄
- 阻塞和非阻塞IO
- 阻塞和非阻塞簡介
- 等待隊列
- 輪詢
- Linux 驅動下的poll 操作函數
- 阻塞IO 實驗
- 非阻塞IO 實驗
阻塞和非阻塞IO 是Linux 驅動開發里面很常見的兩種設備訪問模式,在編寫驅動的時候一定要考慮到阻塞和非阻塞。本章我們就來學習一下阻塞和非阻塞IO,以及如何在驅動程序中處理阻塞與非阻塞,如何在驅動程序使用等待隊列和poll 機制。
阻塞和非阻塞IO
阻塞和非阻塞簡介
這里的“IO”并不是我們學習STM32 或者其他單片機的時候所說的“GPIO”(也就是引腳)。這里的IO 指的是Input/Output,也就是輸入/輸出,是應用程序對驅動設備的輸入/輸出操作。
阻塞:當應用程序對設備驅動進行操作的時候,如果不能獲取到設備資源,那么阻塞式IO 就會將應用程序對應的線程掛起,直到設備資源可以獲取為止。
線程的掛起操作實質上就是線程進入"非可執行"狀態下,在這個狀態下CPU不會分給線程時間片,進入這個狀態可以用來暫停一個線程的運行。線程掛起后,可以通過重新喚醒線程來使之恢復運行。cpu分配的線程片非常的短、同時也非常珍貴。避免資源的浪費。
非阻塞:對于非阻塞IO,應用程序對應的線程不會掛起,它要么一直輪詢等待,直到設備資源可以使用,要么就直接放棄。阻塞式IO 如圖52.1.1.1所示:
圖52.1.1.1 中應用程序調用read 函數從設備中讀取數據,當設備不可用或數據未準備好的時候就會進入到休眠態。等設備可用的時候就會從休眠態喚醒,然后從設備中讀取數據返回給應用程序。非阻塞IO 如圖52.1.2 所示:
從圖52.1.1.2 可以看出,應用程序使用非阻塞訪問方式從設備讀取數據,當設備不可用或數據未準備好的時候會立即向內核返回一個錯誤碼,表示數據讀取失敗。應用程序會再次重新讀取數據,這樣一直往復循環,直到數據讀取成功。
應用程序可以使用如下所示示例代碼來實現阻塞訪問:
1 int fd
;
2 int data
= 0;
3
4 fd
= open("/dev/xxx_dev", O_RDWR
);
5 ret
= read(fd
, &data
, sizeof(data
));
從示例代碼52.1.1.1 可以看出,對于設備驅動文件的默認讀取方式就是阻塞式的,所以我們前面所有的例程測試APP 都是采用阻塞IO。
如果應用程序要采用非阻塞的方式來訪問驅動設備文件,可以使用如下所示代碼:
1 int fd
;
2 int data
= 0;
3
4 fd
= open("/dev/xxx_dev", O_RDWR
| O_NONBLOCK
);
5 ret
= read(fd
, &data
, sizeof(data
));
第4 行使用open 函數打開“/dev/xxx_dev”設備文件的時候添加了參數“O_NONBLOCK”,表示以非阻塞方式打開設備,這樣從設備中讀取數據的時候就是非阻塞方式的了。
等待隊列
1、等待隊列頭
阻塞訪問最大的好處就是當設備文件不可操作的時候進程可以進入休眠態,這樣可以將CPU 資源讓出來。但是,當設備文件可以操作的時候就必須喚醒進程,一般在中斷函數里面完成喚醒工作。Linux 內核提供了等待隊列(wait queue)來實現阻塞進程的喚醒工作,如果我們要在驅動中使用等待隊列,必須創建并初始化一個等待隊列頭,等待隊列頭使用結構體
wait_queue_head_t 表示,wait_queue_head_t 結構體定義在文件include/linux/wait.h 中,結構體內容如下所示:
39 struct __wait_queue_head
{
40 spinlock_t lock
;
41 struct list_head task_list
;
42 };
43 typedef struct __wait_queue_head wait_queue_head_t
;
定義好等待隊列頭以后需要初始化,使用init_waitqueue_head 函數初始化等待隊列頭,函數原型如下:
void init_waitqueue_head(wait_queue_head_t
*q
)
參數q 就是要初始化的等待隊列頭。
也可以使用宏DECLARE_WAIT_QUEUE_HEAD 來一次性完成等待隊列頭的定義的初始化。
2、等待隊列項
等待隊列頭就是一個等待隊列的頭部,每個訪問設備的進程都是一個隊列項,當設備不可用的時候就要將這些進程對應的等待隊列項添加到等待隊列里面。結構體wait_queue_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
;
使用宏DECLARE_WAITQUEUE 定義并初始化一個等待隊列項,宏的內容如下:
DECLARE_WAITQUEUE(name
, tsk
)
name 就是等待隊列項的名字,tsk 表示這個等待隊列項屬于哪個任務(進程),一般設置為current ,在Linux 內核中current 相當于一個全局變量,表示當前進程。因此宏DECLARE_WAITQUEUE 就是給當前正在運行的進程創建并初始化了一個等待隊列項。
3、將隊列項添加/移除等待隊列頭
當設備不可訪問的時候就需要將進程對應的等待隊列項添加到前面創建的等待隊列頭中,只有添加到等待隊列頭中以后進程才能進入休眠態。當設備可以訪問以后再將進程對應的等待隊列項從等待隊列頭中移除即可,等待隊列項添加API 函數如下:
void add_wait_queue(wait_queue_head_t
*q
,
wait_queue_t
*wait
)
函數參數和返回值含義如下:
q:等待隊列項要加入的等待隊列頭。
wait:要加入的等待隊列項。
返回值:無。
等待隊列項移除API 函數如下:
void remove_wait_queue(wait_queue_head_t
*q
,
wait_queue_t
*wait
)
函數參數和返回值含義如下:
q:要刪除的等待隊列項所處的等待隊列頭。
wait:要刪除的等待隊列項。
返回值:無。
4、等待喚醒
當設備可以使用的時候就要喚醒進入休眠態的進程,喚醒可以使用如下兩個函數:
void wake_up(wait_queue_head_t
*q
)
void wake_up_interruptible(wait_queue_head_t
*q
)
參數q 就是要喚醒的等待隊列頭,這兩個函數會將這個等待隊列頭中的所有進程都喚醒。
wake_up 函數可以喚醒處于TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE 狀態的進程,而wake_up_interruptible 函數只能喚醒處于TASK_INTERRUPTIBLE 狀態的進程。
5、等待事件
除了主動喚醒以外,也可以設置等待隊列等待某個事件,當這個事件滿足以后就自動喚醒等待隊列中的進程,和等待事件有關的API 函數如表52.1.2.1 所示:
函數描述
| wait_event(wq, condition) | 等待以wq 為等待隊列頭的等待隊列被喚醒,前提是condition 條件必須滿足(為真),否則一直阻塞。此函數會將進程設置為TASK_UNINTERRUPTIBLE 狀態 |
| wait_event_timeout(wq, condition, timeout) | 功能和wait_event 類似,但是此函數可以添加超時時間,以jiffies 為單位。此函數有返回值,如果返回0 的話表示超時時間到,而且condition為假。為1 的話表示condition 為真,也就是條件滿足了。 |
| wait_event_interruptible(wq, condition) | 與wait_event 函數類似,但是此函數將進程設置為TASK_INTERRUPTIBLE,就是可以被信號打斷。 |
| wait_event_interruptible_timeout(wq, condition, timeout) | 與wait_event_timeout 函數類似,此函數也將進程設置為TASK_INTERRUPTIBLE,可以被信號打斷。 |
輪詢
如果用戶應用程序以非阻塞的方式訪問設備,設備驅動程序就要提供非阻塞的處理方式,也就是輪詢。poll、epoll 和select 可以用于處理輪詢,應用程序通過select、epoll 或poll 函數來查詢設備是否可以操作,如果可以操作的話就從設備讀取或者向設備寫入數據。當應用程序調用select、epoll 或poll 函數的時候設備驅動程序中的poll 函數就會執行,因此需要在設備驅動程序中編寫poll 函數。我們先來看一下應用程序中使用的select、poll 和epoll 這三個函數。
1、select 函數
select 函數原型如下:
int select(int nfds
,
fd_set
*readfds
,
fd_set
*writefds
,
fd_set
*exceptfds
,
struct timeval
*timeout
)
函數參數和返回值含義如下:
nfds:所要監視的這三類文件描述集合中,最大文件描述符加1。
readfds、writefds 和exceptfds:這三個指針指向描述符集合,這三個參數指明了關心哪些描述符、需要滿足哪些條件等等,這三個參數都是fd_set 類型的,fd_set 類型變量的每一個位都代表了一個文件描述符。readfds 用于監視指定描述符集的讀變化,也就是監視這些文件是否可以讀取,只要這些集合里面有一個文件可以讀取那么seclect 就會返回一個大于0 的值表示文件可以讀取。如果沒有文件可以讀取,那么就會根據timeout 參數來判斷是否超時??梢詫eadfs設置為NULL,表示不關心任何文件的讀變化。writefds 和readfs 類似,只是writefs 用于監視這些文件是否可以進行寫操作。exceptfds 用于監視這些文件的異常。
比如我們現在要從一個設備文件中讀取數據,那么就可以定義一個fd_set 變量,這個變量要傳遞給參數readfds。當我們定義好一個fd_set 變量以后可以使用如下所示幾個宏進行操作:
void FD_ZERO(fd_set
*set
)
void FD_SET(int fd
, fd_set
*set
)
void FD_CLR(int fd
, fd_set
*set
)
int FD_ISSET(int fd
, fd_set
*set
)
FD_ZERO 用于將fd_set 變量的所有位都清零,FD_SET 用于將fd_set 變量的某個位置1,也就是向fd_set 添加一個文件描述符,參數fd 就是要加入的文件描述符。FD_CLR 用于將fd_set變量的某個位清零,也就是將一個文件描述符從fd_set 中刪除,參數fd 就是要刪除的文件描述
符。FD_ISSET 用于測試一個文件是否屬于某個集合,參數fd 就是要判斷的文件描述符。
timeout:超時時間,當我們調用select 函數等待某些文件描述符可以設置超時時間,超時時間使用結構體timeval 表示,結構體定義如下所示:
struct timeval
{
long tv_sec
;
long tv_usec
;
};
當timeout 為NULL 的時候就表示無限期的等待。
返回值:0,表示的話就表示超時發生,但是沒有任何文件描述符可以進行操作;-1,發生錯誤;其他值,可以進行操作的文件描述符個數。
使用select 函數對某個設備驅動文件進行讀非阻塞訪問的操作示例如下所示:
1 void main(void)
2 {
3 int ret
, fd
;
4 fd_set readfds
;
5 struct timeval timeout
;
6
7 fd
= open("dev_xxx", O_RDWR
| O_NONBLOCK
);
8
9 FD_ZERO(&readfds
);
10 FD_SET(fd
, &readfds
);
11
12
13 timeout
.tv_sec
= 0;
14 timeout
.tv_usec
= 500000;
15
16 ret
= select(fd
+ 1, &readfds
, NULL, NULL, &timeout
);
17 switch (ret
) {
18 case 0:
19 printf("timeout!\r\n");
20 break;
21 case -1:
22 printf("error!\r\n");
23 break;
24 default:
25 if(FD_ISSET(fd
, &readfds
)) {
26
27 }
28 break;
29 }
30 }
2、poll 函數
在單個線程中,select 函數能夠監視的文件描述符數量有最大的限制,一般為1024,可以修改內核將監視的文件描述符數量改大,但是這樣會降低效率!這個時候就可以使用poll 函數,poll 函數本質上和select 沒有太大的差別,但是poll 函數沒有最大文件描述符限制,Linux 應用程序中poll 函數原型如下所示:
int poll(struct pollfd
*fds
,
nfds_t nfds
,
int timeout
)
函數參數和返回值含義如下:
fds:要監視的文件描述符集合以及要監視的事件,為一個數組,數組元素都是結構體pollfd類型的,pollfd 結構體如下所示:
struct pollfd
{
int fd
;
short events
;
short revents
;
};
fd 是要監視的文件描述符,如果fd 無效的話那么events 監視事件也就無效,并且revents返回0。events 是要監視的事件,可監視的事件類型如下所示:
POLLIN 有數據可以讀取。
POLLPRI 有緊急的數據需要讀取。
POLLOUT 可以寫數據。
POLLERR 指定的文件描述符發生錯誤。
POLLHUP 指定的文件描述符掛起。
POLLNVAL 無效的請求。
POLLRDNORM 等同于POLLIN
revents 是返回參數,也就是返回的事件,由Linux 內核設置具體的返回事件。
nfds:poll 函數要監視的文件描述符數量。
timeout:超時時間,單位為ms。
返回值:返回revents 域中不為0 的pollfd 結構體個數,也就是發生事件或錯誤的文件描述符數量;0,超時;-1,發生錯誤,并且設置errno 為錯誤類型。
使用poll 函數對某個設備驅動文件進行讀非阻塞訪問的操作示例如下所示:
1 void main(void)
2 {
3 int ret
;
4 int fd
;
5 struct pollfd fds
;
6
7 fd
= open(filename
, O_RDWR
| O_NONBLOCK
);
8
9
10 fds
.fd
= fd
;
11 fds
.events
= POLLIN
;
12
13 ret
= poll(&fds
, 1, 500);
14 if (ret
) {
15 ......
16
17 ......
18 } else if (ret
== 0) {
19 ......
20 } else if (ret
< 0) {
21 ......
22 }
23 }
3、epoll 函數
傳統的selcet 和poll 函數都會隨著所監聽的fd 數量的增加,出現效率低下的問題,而且poll 函數每次必須遍歷所有的描述符來檢查就緒的描述符,這個過程很浪費時間。為此,epoll應運而生,epoll 就是為處理大并發而準備的,一般常常在網絡編程中使用epoll 函數。應用程序需要先使用epoll_create 函數創建一個epoll 句柄,epoll_create 函數原型如下:
int epoll_create(int size
)
函數參數和返回值含義如下:
size:從Linux2.6.8 開始此參數已經沒有意義了,隨便填寫一個大于0 的值就可以。
返回值:epoll 句柄,如果為-1 的話表示創建失敗。
epoll 句柄創建成功以后使用epoll_ctl 函數向其中添加要監視的文件描述符以及監視的事件,epoll_ctl 函數原型如下所示:
int epoll_ctl(int epfd
,
int op
,
int fd
,
struct epoll_event
*event
)
函數參數和返回值含義如下:
epfd:要操作的epoll 句柄,也就是使用epoll_create 函數創建的epoll 句柄。
op:表示要對epfd(epoll 句柄)進行的操作,可以設置為:
EPOLL_CTL_ADD 向epfd 添加文件參數fd 表示的描述符。
EPOLL_CTL_MOD 修改參數fd 的event 事件。
EPOLL_CTL_DEL 從epfd 中刪除fd 描述符。
fd:要監視的文件描述符。
event:要監視的事件類型,為epoll_event 結構體類型指針,epoll_event 結構體類型如下所示:
struct epoll_event
{
uint32_t events
;
epoll_data_t data
;
};
結構體epoll_event 的events 成員變量表示要監視的事件,可選的事件如下所示:
EPOLLIN 有數據可以讀取。
EPOLLOUT 可以寫數據。
EPOLLPRI 有緊急的數據需要讀取。
EPOLLERR 指定的文件描述符發生錯誤。
EPOLLHUP 指定的文件描述符掛起。
EPOLLET 設置epoll 為邊沿觸發,默認觸發模式為水平觸發。
EPOLLONESHOT 一次性的監視,當監視完成以后還需要再次監視某個fd,那么就需要將
fd 重新添加到epoll 里面。
上面這些事件可以進行“或”操作,也就是說可以設置監視多個事件。
返回值:0,成功;-1,失敗,并且設置errno 的值為相應的錯誤碼。
一切都設置好以后應用程序就可以通過epoll_wait 函數來等待事件的發生,類似select 函數。epoll_wait 函數原型如下所示:
int epoll_wait(int epfd
,
struct epoll_event
*events
,
int maxevents
,
int timeout
)
函數參數和返回值含義如下:
epfd:要等待的epoll。
events:指向epoll_event 結構體的數組,當有事件發生的時候Linux 內核會填寫events,調用者可以根據events 判斷發生了哪些事件。
maxevents:events 數組大小,必須大于0。
timeout:超時時間,單位為ms。
返回值:0,超時;-1,錯誤;其他值,準備就緒的文件描述符數量。
epoll 更多的是用在大規模的并發服務器上,因為在這種場合下select 和poll 并不適合。當設計到的文件描述符(fd)比較少的時候就適合用selcet 和poll,本章我們就使用sellect 和poll 這兩個函數。
Linux 驅動下的poll 操作函數
當應用程序調用select 或poll 函數來對驅動程序進行非阻塞訪問的時候,驅動程序file_operations 操作集中的poll 函數就會執行。所以驅動程序的編寫者需要提供對應的poll 函數,poll 函數原型如下所示:
unsigned int (*poll
) (struct file
*filp
, struct poll_table_struct
*wait
)
函數參數和返回值含義如下:
filp:要打開的設備文件(文件描述符)。
wait:結構體poll_table_struct 類型指針,由應用程序傳遞進來的。一般將此參數傳遞給poll_wait 函數。
返回值:向應用程序返回設備或者資源狀態,可以返回的資源狀態如下:
POLLIN 有數據可以讀取。
POLLPRI 有緊急的數據需要讀取。
POLLOUT 可以寫數據。
POLLERR 指定的文件描述符發生錯誤。
POLLHUP 指定的文件描述符掛起。
POLLNVAL 無效的請求。
POLLRDNORM 等同于POLLIN,普通數據可讀
我們需要在驅動程序的poll 函數中調用poll_wait 函數,poll_wait 函數不會引起阻塞,只是將應用程序添加到poll_table 中,poll_wait 函數原型如下:
void poll_wait(struct file
* filp
, wait_queue_head_t
* wait_address
, poll_table
*p
)
參數wait_address 是要添加到poll_table 中的等待隊列頭,參數p 就是poll_table,就是file_operations 中poll 函數的wait 參數。
阻塞IO 實驗
在上一章Linux 中斷實驗中,我們直接在應用程序中通過read 函數不斷的讀取按鍵狀態,當按鍵有效的時候就打印出按鍵值。這種方法有個缺點,那就是imx6uirqApp 這個測試應用程序擁有很高的CPU 占用率,大家可以在開發板中加載上一章的驅動程序模塊imx6uirq.ko,然后以后臺運行模式打開imx6uirqApp 這個測試軟件,命令如下:
./imx6uirqApp
/dev
/imx6uirq
&
測試驅動是否正常工作,如果驅動工作正常的話輸入“top”命令查看imx6uirqApp 這個應用程序的CPU 使用率,結果如圖52.2.1 所示:
從圖52.2.1 可以看出,imx6uirqApp 這個應用程序的CPU 使用率竟然高達99.6%,這僅僅是一個讀取按鍵值的應用程序,這么高的CPU 使用率顯然是有問題的!原因就在于我們是直接在while 循環中通過read 函數讀取按鍵值,因此imx6uirqApp 這個軟件會一直運行,一直讀取按鍵值,CPU 使用率肯定就會很高。最好的方法就是在沒有有效的按鍵事件發生的時候,
imx6uirqApp 這個應用程序應該處于休眠狀態,當有按鍵事件發生以后imx6uirqApp 這個應用程序才運行,打印出按鍵值,這樣就會降低CPU 使用率,本小節我們就使用阻塞IO 來實現此功能。
硬件原理圖分析
本章實驗硬件原理圖參考15.2 小節即可。
實驗程序編寫
1、驅動程序編寫
本實驗對應的例程路徑為:開發板光盤-> 2、Linux 驅動例程-> 14_blockio。
本章實驗我們在上一章的“13_irq”實驗的基礎上完成,主要是對其添加阻塞訪問相關的代碼。新建名為“14_blockio”的文件夾,然后在14_blockio 文件夾里面創建vscode 工程,工作區命名為“blockio”。將“13_irq”實驗中的imx6uirq.c 復制到14_blockio 文件夾中,并重命名為blockio.c。接下來我們就修改blockio.c 這個文件,在其中添加阻塞相關的代碼,完成以后的
blockio.c 內容如下所示(因為是在上一章實驗的imx6uirq.c 文件的基礎上修改的,為了減少篇幅,下面的代碼有省略):
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define IMX6UIRQ_CNT 1
#define IMX6UIRQ_NAME "blockio"
#define KEY0VALUE 0X01
#define INVAKEY 0XFF
#define KEY_NUM 1
struct irq_keydesc
{int gpio
; int irqnum
; unsigned char value
; char name
[10]; irqreturn_t
(*handler
)(int, void *);
};
struct imx6uirq_dev
{dev_t devid
; struct cdev cdev
; struct class
*class
; struct device
*device
; int major
; int minor
; struct device_node
*nd
; atomic_t keyvalue
; atomic_t releasekey
; struct timer_list timer
;struct irq_keydesc irqkeydesc
[KEY_NUM
]; unsigned char curkeynum
; wait_queue_head_t r_wait
;
};struct imx6uirq_dev imx6uirq
;
static irqreturn_t
key0_handler(int irq
, void *dev_id
)
{struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)dev_id
;dev
->curkeynum
= 0;dev
->timer
.data
= (volatile long)dev_id
;mod_timer(&dev
->timer
, jiffies
+ msecs_to_jiffies(10)); return IRQ_RETVAL(IRQ_HANDLED
);
}
void timer_function(unsigned long arg
)
{unsigned char value
;unsigned char num
;struct irq_keydesc
*keydesc
;struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)arg
;num
= dev
->curkeynum
;keydesc
= &dev
->irqkeydesc
[num
];value
= gpio_get_value(keydesc
->gpio
); if(value
== 0){ atomic_set(&dev
->keyvalue
, keydesc
->value
);}else{ atomic_set(&dev
->keyvalue
, 0x80 | keydesc
->value
);atomic_set(&dev
->releasekey
, 1); } if(atomic_read(&dev
->releasekey
)) { wake_up_interruptible(&dev
->r_wait
);}
}
static int keyio_init(void)
{unsigned char i
= 0;char name
[10];int ret
= 0;imx6uirq
.nd
= of_find_node_by_path("/key");if (imx6uirq
.nd
== NULL){printk("key node not find!\r\n");return -EINVAL
;} for (i
= 0; i
< KEY_NUM
; i
++) {imx6uirq
.irqkeydesc
[i
].gpio
= of_get_named_gpio(imx6uirq
.nd
,"key-gpio", i
);if (imx6uirq
.irqkeydesc
[i
].gpio
< 0) {printk("can't get key%d\r\n", i
);}}for (i
= 0; i
< KEY_NUM
; i
++) {memset(imx6uirq
.irqkeydesc
[i
].name
, 0, sizeof(name
)); sprintf(imx6uirq
.irqkeydesc
[i
].name
, "KEY%d", i
); gpio_request(imx6uirq
.irqkeydesc
[i
].gpio
, name
);gpio_direction_input(imx6uirq
.irqkeydesc
[i
].gpio
); imx6uirq
.irqkeydesc
[i
].irqnum
= irq_of_parse_and_map(imx6uirq
.nd
, i
);
#if 0imx6uirq
.irqkeydesc
[i
].irqnum
= gpio_to_irq(imx6uirq
.irqkeydesc
[i
].gpio
);
#endif}imx6uirq
.irqkeydesc
[0].handler
= key0_handler
;imx6uirq
.irqkeydesc
[0].value
= KEY0VALUE
;for (i
= 0; i
< KEY_NUM
; i
++) {ret
= request_irq(imx6uirq
.irqkeydesc
[i
].irqnum
, imx6uirq
.irqkeydesc
[i
].handler
, IRQF_TRIGGER_FALLING
|IRQF_TRIGGER_RISING
, imx6uirq
.irqkeydesc
[i
].name
, &imx6uirq
);if(ret
< 0){printk("irq %d request failed!\r\n", imx6uirq
.irqkeydesc
[i
].irqnum
);return -EFAULT
;}}init_timer(&imx6uirq
.timer
);imx6uirq
.timer
.function
= timer_function
;init_waitqueue_head(&imx6uirq
.r_wait
);return 0;
}
static int imx6uirq_open(struct inode
*inode
, struct file
*filp
)
{filp
->private_data
= &imx6uirq
; return 0;
}
static ssize_t
imx6uirq_read(struct file
*filp
, char __user
*buf
, size_t cnt
, loff_t
*offt
)
{int ret
= 0;unsigned char keyvalue
= 0;unsigned char releasekey
= 0;struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)filp
->private_data
;#if 0ret
= wait_event_interruptible(dev
->r_wait
, atomic_read(&dev
->releasekey
)); if (ret
) {goto wait_error
;}
#endifDECLARE_WAITQUEUE(wait
, current
); if(atomic_read(&dev
->releasekey
) == 0) { add_wait_queue(&dev
->r_wait
, &wait
); __set_current_state(TASK_INTERRUPTIBLE
);schedule(); if(signal_pending(current
)) { ret
= -ERESTARTSYS
;goto wait_error
;}__set_current_state(TASK_RUNNING
); remove_wait_queue(&dev
->r_wait
, &wait
); }keyvalue
= atomic_read(&dev
->keyvalue
);releasekey
= atomic_read(&dev
->releasekey
);if (releasekey
) { if (keyvalue
& 0x80) {keyvalue
&= ~0x80;ret
= copy_to_user(buf
, &keyvalue
, sizeof(keyvalue
));} else {goto data_error
;}atomic_set(&dev
->releasekey
, 0);} else {goto data_error
;}return 0;wait_error
:set_current_state(TASK_RUNNING
); remove_wait_queue(&dev
->r_wait
, &wait
); return ret
;data_error
:return -EINVAL
;
}
static struct file_operations imx6uirq_fops
= {.owner
= THIS_MODULE
,.open
= imx6uirq_open
,.read
= imx6uirq_read
,
};
static int __init
imx6uirq_init(void)
{if (imx6uirq
.major
) {imx6uirq
.devid
= MKDEV(imx6uirq
.major
, 0);register_chrdev_region(imx6uirq
.devid
, IMX6UIRQ_CNT
, IMX6UIRQ_NAME
);} else {alloc_chrdev_region(&imx6uirq
.devid
, 0, IMX6UIRQ_CNT
, IMX6UIRQ_NAME
);imx6uirq
.major
= MAJOR(imx6uirq
.devid
);imx6uirq
.minor
= MINOR(imx6uirq
.devid
);}cdev_init(&imx6uirq
.cdev
, &imx6uirq_fops
);cdev_add(&imx6uirq
.cdev
, imx6uirq
.devid
, IMX6UIRQ_CNT
);imx6uirq
.class
= class_create(THIS_MODULE
, IMX6UIRQ_NAME
);if (IS_ERR(imx6uirq
.class
)) { return PTR_ERR(imx6uirq
.class
);}imx6uirq
.device
= device_create(imx6uirq
.class
, NULL, imx6uirq
.devid
, NULL, IMX6UIRQ_NAME
);if (IS_ERR(imx6uirq
.device
)) {return PTR_ERR(imx6uirq
.device
);}atomic_set(&imx6uirq
.keyvalue
, INVAKEY
);atomic_set(&imx6uirq
.releasekey
, 0);keyio_init();return 0;
}
static void __exit
imx6uirq_exit(void)
{unsigned i
= 0;del_timer_sync(&imx6uirq
.timer
); for (i
= 0; i
< KEY_NUM
; i
++) {free_irq(imx6uirq
.irqkeydesc
[i
].irqnum
, &imx6uirq
);}cdev_del(&imx6uirq
.cdev
);unregister_chrdev_region(imx6uirq
.devid
, IMX6UIRQ_CNT
);device_destroy(imx6uirq
.class
, imx6uirq
.devid
);class_destroy(imx6uirq
.class
);
}module_init(imx6uirq_init
);
module_exit(imx6uirq_exit
);
MODULE_LICENSE("GPL");
第32 行,修改設備文件名字為“blockio”,當驅動程序加載成功以后就會在根文件系統中出現一個名為“/dev/blockio”的文件。
第61 行,在設備結構體中添加一個等待隊列頭r_wait,因為在Linux 驅動中處理阻塞IO需要用到等待隊列。
第107~110 行,定時器中斷處理函數執行,表示有按鍵按下,先在107 行判斷一下是否是一次有效的按鍵,如果是的話就通過wake_up 或者wake_up_interruptible 函數來喚醒等待隊列r_wait。
第168 行,調用init_waitqueue_head 函數初始化等待隊列頭r_wait。
第200~206 行,采用等待事件來處理read 的阻塞訪問,wait_event_interruptible 函數等待releasekey 有效,也就是有按鍵按下。如果按鍵沒有按下的話進程就會進入休眠狀態,因為采用了wait_event_interruptible 函數,因此進入休眠態的進程可以被信號打斷。
第208~218 行,首先使用DECLARE_WAITQUEUE 宏定義一個等待隊列,如果沒有按鍵按下的話就使用add_wait_queue 函數將當前任務的等待隊列添加到等待隊列頭r_wait 中。隨后調用__set_current_state 函數設置當前進程的狀態為TASK_INTERRUPTIBLE,也就是可以被信號打斷。接下來調用schedule 函數進行一次任務切換,當前進程就會進入到休眠態。如果有按
鍵按下,那么進入休眠態的進程就會喚醒,然后接著從休眠點開始運行。在這里也就是從第213行開始運行,首先通過signal_pending 函數判斷一下進程是不是由信號喚醒的,如果是由信號喚醒的話就直接返回-ERESTARTSYS 這個錯誤碼。如果不是由信號喚醒的(也就是被按鍵喚醒的)那么就在217 行調用__set_current_state 函數將任務狀態設置為TASK_RUNNING,然后在
218 行調用remove_wait_queue 函數將進程從等待隊列中刪除。
使用等待隊列實現阻塞訪問重點注意兩點:
①、將任務或者進程加入到等待隊列頭,
②、在合適的點喚醒等待隊列,一般都是中斷處理函數里面。
2、編寫測試APP
本節實驗的測試APP 直接使用第51.3.3 小節所編寫的imx6uirqApp.c,將imx6uirqApp.c 復制到本節實驗文件夾下,并且重命名為blockioApp.c,不需要修改任何內容。
運行測試
1、編譯驅動程序和測試APP
①、編譯驅動程序
編寫Makefile 文件,本章實驗的Makefile 文件和第四十章實驗基本一樣,只是將obj-m 變量的值改為blockio.o,Makefile 內容如下所示:
KERNELDIR
:= /home
/zuozhongkai
/linux
/IMX6ULL
/linux
/temp
/linux
-imx
-rel_imx_4
.1.15_2
.1.0_ga_alientek
CURRENT_PATH
:= $
(shell pwd
)obj
-m
:= blockio
.obuild
: kernel_moduleskernel_modules
:$
(MAKE
) -C $
(KERNELDIR
) M
=$
(CURRENT_PATH
) modulesclean
:$
(MAKE
) -C $
(KERNELDIR
) M
=$
(CURRENT_PATH
) clean
第4 行,設置obj-m 變量的值為blockio.o。
輸入如下命令編譯出驅動模塊文件:
make
-j32
編譯成功以后就會生成一個名為“blockio.ko”的驅動模塊文件。
②、編譯測試APP
輸入如下命令編譯測試noblockioApp.c 這個測試程序:
arm
-linux
-gnueabihf
-gcc blockioApp
.c
-o blockioApp
編譯成功以后就會生成blcokioApp 這個應用程序。
2、運行測試
將上一小節編譯出來blockio.ko 和blockioApp 這兩個文件拷貝到rootfs/lib/modules/4.1.15目錄中,重啟開發板,進入到目錄lib/modules/4.1.15 中,輸入如下命令加載blockio.ko 驅動模塊:
depmod
modprobe blockio
.ko
驅動加載成功以后使用如下命令打開blockioApp 這個測試APP,并且以后臺模式運行:
./blockioApp
/dev
/blockio
&
按下開發板上的KEY0 按鍵,結果如圖52.2.3.1 所示:
當按下KEY0 按鍵以后blockioApp 這個測試APP 就會打印出按鍵值。輸入“top”命令,查看blockioAPP 這個應用APP 的CPU 使用率,如圖52.2.3.2 所示:
從圖52.2.3.2 可以看出,當我們在按鍵驅動程序里面加入阻塞訪問以后,blockioApp 這個應用程序的CPU 使用率從圖52.2.1 中的99.6%降低到了0.0%。大家注意,這里的0.0%并不是說blockioApp 這個應用程序不使用CPU 了,只是因為使用率太小了,CPU 使用率可能為0.00001%,但是圖52.2.3.2 只能顯示出小數點后一位,因此就顯示成了0.0%。
我們可以使用“kill”命令關閉后臺運行的應用程序,比如我們關閉掉blockioApp 這個后臺運行的應用程序。首先輸出“Ctrl+C”關閉top 命令界面,進入到命令行模式。然后使用“ps”命令查看一下blockioApp 這個應用程序的PID,如圖52.2.3.3 所示:
從圖圖52.2.3.3 可以看出,blockioApp 這個應用程序的PID 為149,使用“kill -9 PID”即可“殺死”指定PID 的進程,比如我們現在要“殺死”PID 為149 的blockioApp 應用程序,可是使用如下命令:
kill
-9 149
輸入上述命令以后終端顯示如圖52.2.3.4 所示:
從圖52.2.3.4 可以看出,“./blockioApp /dev/blockio”這個應用程序已經被“殺掉”了,在此輸入“ps”命令查看當前系統運行的進程,會發現blockioApp 已經不見了。這個就是使用kill命令“殺掉”指定進程的方法。
非阻塞IO 實驗
硬件原理圖分析
本章實驗硬件原理圖參考15.2 小節即可。
實驗程序編寫
1、驅動程序編寫
本實驗對應的例程路徑為:開發板光盤-> 2、Linux 驅動例程-> 15_noblockio。
本章實驗我們在52.2 小節中的“14_blockio”實驗的基礎上完成,上一小節實驗我們已經在驅動中添加了阻塞IO 的代碼,本小節我們繼續完善驅動,加入非阻塞IO 驅動代碼。新建名為“15_noblockio”的文件夾,然后在15_noblockio 文件夾里面創建vscode 工程,工作區命名為“noblockio”。將“14_blockio”實驗中的blockio.c 復制到15_noblockio 文件夾中,并重命名為noblockio.c。接下來我們就修改noblockio.c 這個文件,在其中添加非阻塞相關的代碼,完成
以后的noblockio.c 內容如下所示(因為是在上一小節實驗的blockio.c 文件的基礎上修改的,為了減少篇幅,下面的代碼有省略):
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define IMX6UIRQ_CNT 1
#define IMX6UIRQ_NAME "noblockio"
#define KEY0VALUE 0X01
#define INVAKEY 0XFF
#define KEY_NUM 1
struct irq_keydesc
{int gpio
; int irqnum
; unsigned char value
; char name
[10]; irqreturn_t
(*handler
)(int, void *);
};
struct imx6uirq_dev
{dev_t devid
; struct cdev cdev
; struct class
*class
; struct device
*device
; int major
; int minor
; struct device_node
*nd
; atomic_t keyvalue
; atomic_t releasekey
; struct timer_list timer
;struct irq_keydesc irqkeydesc
[KEY_NUM
]; unsigned char curkeynum
; wait_queue_head_t r_wait
;
};struct imx6uirq_dev imx6uirq
;
static irqreturn_t
key0_handler(int irq
, void *dev_id
)
{struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)dev_id
;dev
->curkeynum
= 0;dev
->timer
.data
= (volatile long)dev_id
;mod_timer(&dev
->timer
, jiffies
+ msecs_to_jiffies(10)); return IRQ_RETVAL(IRQ_HANDLED
);
}
void timer_function(unsigned long arg
)
{unsigned char value
;unsigned char num
;struct irq_keydesc
*keydesc
;struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)arg
;num
= dev
->curkeynum
;keydesc
= &dev
->irqkeydesc
[num
];value
= gpio_get_value(keydesc
->gpio
); if(value
== 0){ atomic_set(&dev
->keyvalue
, keydesc
->value
);}else{ atomic_set(&dev
->keyvalue
, 0x80 | keydesc
->value
);atomic_set(&dev
->releasekey
, 1); } if(atomic_read(&dev
->releasekey
)) { wake_up_interruptible(&dev
->r_wait
);}
}
static int keyio_init(void)
{unsigned char i
= 0;char name
[10];int ret
= 0;imx6uirq
.nd
= of_find_node_by_path("/key");if (imx6uirq
.nd
== NULL){printk("key node not find!\r\n");return -EINVAL
;} for (i
= 0; i
< KEY_NUM
; i
++) {imx6uirq
.irqkeydesc
[i
].gpio
= of_get_named_gpio(imx6uirq
.nd
,"key-gpio", i
);if (imx6uirq
.irqkeydesc
[i
].gpio
< 0) {printk("can't get key%d\r\n", i
);}}for (i
= 0; i
< KEY_NUM
; i
++) {memset(imx6uirq
.irqkeydesc
[i
].name
, 0, sizeof(name
)); sprintf(imx6uirq
.irqkeydesc
[i
].name
, "KEY%d", i
); gpio_request(imx6uirq
.irqkeydesc
[i
].gpio
, name
);gpio_direction_input(imx6uirq
.irqkeydesc
[i
].gpio
); imx6uirq
.irqkeydesc
[i
].irqnum
= irq_of_parse_and_map(imx6uirq
.nd
, i
);
#if 0imx6uirq
.irqkeydesc
[i
].irqnum
= gpio_to_irq(imx6uirq
.irqkeydesc
[i
].gpio
);
#endif}imx6uirq
.irqkeydesc
[0].handler
= key0_handler
;imx6uirq
.irqkeydesc
[0].value
= KEY0VALUE
;for (i
= 0; i
< KEY_NUM
; i
++) {ret
= request_irq(imx6uirq
.irqkeydesc
[i
].irqnum
, imx6uirq
.irqkeydesc
[i
].handler
, IRQF_TRIGGER_FALLING
|IRQF_TRIGGER_RISING
, imx6uirq
.irqkeydesc
[i
].name
, &imx6uirq
);if(ret
< 0){printk("irq %d request failed!\r\n", imx6uirq
.irqkeydesc
[i
].irqnum
);return -EFAULT
;}}init_timer(&imx6uirq
.timer
);imx6uirq
.timer
.function
= timer_function
;init_waitqueue_head(&imx6uirq
.r_wait
);return 0;
}
static int imx6uirq_open(struct inode
*inode
, struct file
*filp
)
{filp
->private_data
= &imx6uirq
; return 0;
}
static ssize_t
imx6uirq_read(struct file
*filp
, char __user
*buf
, size_t cnt
, loff_t
*offt
)
{int ret
= 0;unsigned char keyvalue
= 0;unsigned char releasekey
= 0;struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)filp
->private_data
;if (filp
->f_flags
& O_NONBLOCK
) { if(atomic_read(&dev
->releasekey
) == 0) return -EAGAIN
;} else { ret
= wait_event_interruptible(dev
->r_wait
, atomic_read(&dev
->releasekey
)); if (ret
) {goto wait_error
;}}keyvalue
= atomic_read(&dev
->keyvalue
);releasekey
= atomic_read(&dev
->releasekey
);if (releasekey
) { if (keyvalue
& 0x80) {keyvalue
&= ~0x80;ret
= copy_to_user(buf
, &keyvalue
, sizeof(keyvalue
));} else {goto data_error
;}atomic_set(&dev
->releasekey
, 0);} else {goto data_error
;}return 0;wait_error
:return ret
;
data_error
:return -EINVAL
;
}
unsigned int imx6uirq_poll(struct file
*filp
, struct poll_table_struct
*wait
)
{unsigned int mask
= 0;struct imx6uirq_dev
*dev
= (struct imx6uirq_dev
*)filp
->private_data
;poll_wait(filp
, &dev
->r_wait
, wait
); if(atomic_read(&dev
->releasekey
)) { mask
= POLLIN
| POLLRDNORM
; }return mask
;
}
static struct file_operations imx6uirq_fops
= {.owner
= THIS_MODULE
,.open
= imx6uirq_open
,.read
= imx6uirq_read
,.poll
= imx6uirq_poll
,
};
static int __init
imx6uirq_init(void)
{if (imx6uirq
.major
) {imx6uirq
.devid
= MKDEV(imx6uirq
.major
, 0);register_chrdev_region(imx6uirq
.devid
, IMX6UIRQ_CNT
, IMX6UIRQ_NAME
);} else {alloc_chrdev_region(&imx6uirq
.devid
, 0, IMX6UIRQ_CNT
, IMX6UIRQ_NAME
);imx6uirq
.major
= MAJOR(imx6uirq
.devid
);imx6uirq
.minor
= MINOR(imx6uirq
.devid
);}cdev_init(&imx6uirq
.cdev
, &imx6uirq_fops
);cdev_add(&imx6uirq
.cdev
, imx6uirq
.devid
, IMX6UIRQ_CNT
);imx6uirq
.class
= class_create(THIS_MODULE
, IMX6UIRQ_NAME
);if (IS_ERR(imx6uirq
.class
)) { return PTR_ERR(imx6uirq
.class
);}imx6uirq
.device
= device_create(imx6uirq
.class
, NULL, imx6uirq
.devid
, NULL, IMX6UIRQ_NAME
);if (IS_ERR(imx6uirq
.device
)) {return PTR_ERR(imx6uirq
.device
);}atomic_set(&imx6uirq
.keyvalue
, INVAKEY
);atomic_set(&imx6uirq
.releasekey
, 0);keyio_init();return 0;
}
static void __exit
imx6uirq_exit(void)
{unsigned i
= 0;del_timer_sync(&imx6uirq
.timer
); for (i
= 0; i
< KEY_NUM
; i
++) {free_irq(imx6uirq
.irqkeydesc
[i
].irqnum
, &imx6uirq
);}cdev_del(&imx6uirq
.cdev
);unregister_chrdev_region(imx6uirq
.devid
, IMX6UIRQ_CNT
);device_destroy(imx6uirq
.class
, imx6uirq
.devid
);class_destroy(imx6uirq
.class
);
} module_init(imx6uirq_init
);
module_exit(imx6uirq_exit
);
MODULE_LICENSE("GPL");
第32 行,修改設備文件名字為“noblockio”,當驅動程序加載成功以后就會在根文件系統中出現一個名為“/dev/noblockio”的文件。
第202~204 行,判斷是否為非阻塞式讀取訪問,如果是的話就判斷按鍵是否有效,也就是判斷一下有沒有按鍵按下,如果沒有的話就返回-EAGAIN。
第241~ 252 行,imx6uirq_poll 函數就是file_operations 驅動操作集中的poll 函數,當應用程序調用select 或者poll 函數的時候imx6uirq_poll 函數就會執行。第246 行調用poll_wait 函數將等待隊列頭添加到poll_table 中,第
248~250 行判斷按鍵是否有效,如果按鍵有效的話就向應用程序返回POLLIN 這個事件,表示有數據可以讀取。
第259 行,設置file_operations 的poll 成員變量為imx6uirq_poll。
2、編寫測試APP
新建名為noblockioApp.c 測試APP 文件,然后在其中輸入如下所示內容:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "poll.h"
#include "sys/select.h"
#include "sys/time.h"
#include "linux/ioctl.h"
int main(int argc
, char *argv
[])
{int fd
;int ret
= 0;char *filename
;struct pollfd fds
;fd_set readfds
;struct timeval timeout
;unsigned char data
;if (argc
!= 2) {printf("Error Usage!\r\n");return -1;}filename
= argv
[1];fd
= open(filename
, O_RDWR
| O_NONBLOCK
); if (fd
< 0) {printf("Can't open file %s\r\n", filename
);return -1;}#if 0fds
.fd
= fd
;fds
.events
= POLLIN
;while (1) {ret
= poll(&fds
, 1, 500);if (ret
) { ret
= read(fd
, &data
, sizeof(data
));if(ret
< 0) {} else {if(data
)printf("key value = %d \r\n", data
);} } else if (ret
== 0) { } else if (ret
< 0) { }}
#endifwhile (1) { FD_ZERO(&readfds
);FD_SET(fd
, &readfds
);timeout
.tv_sec
= 0;timeout
.tv_usec
= 500000; ret
= select(fd
+ 1, &readfds
, NULL, NULL, &timeout
);switch (ret
) {case 0: break;case -1: break;default: if(FD_ISSET(fd
, &readfds
)) {ret
= read(fd
, &data
, sizeof(data
));if (ret
< 0) {} else {if (data
)printf("key value=%d\r\n", data
);}}break;} }close(fd
);return ret
;
}
第52~73 行,這段代碼使用poll 函數來實現非阻塞訪問,在while 循環中使用poll 函數不斷的輪詢,檢查驅動程序是否有數據可以讀取,如果可以讀取的話就調用read 函數讀取按鍵數據。
第75~101 行,這段代碼使用select 函數來實現非阻塞訪問。
運行測試
1、編譯驅動程序和測試APP
①、編譯驅動程序
編寫Makefile 文件,本章實驗的Makefile 文件和第四十章實驗基本一樣,只是將obj-m 變量的值改為noblockio.o,Makefile 內容如下所示:
1 KERNELDIR
:= /home
/zuozhongkai
/linux
/IMX6ULL
/linux
/temp
/linux
-imx
-rel_imx_4
.1.15_2
.1.0_ga_alientek
......
4 obj
-m
:= noblockio
.o
......
11 clean
:
12 $
(MAKE
) -C $
(KERNELDIR
) M
=$
(CURRENT_PATH
) clean
第4 行,設置obj-m 變量的值為noblockio.o。
輸入如下命令編譯出驅動模塊文件:
make
-j32
編譯成功以后就會生成一個名為“noblockio.ko”的驅動模塊文件。
②、編譯測試APP
輸入如下命令編譯測試noblockioApp.c 這個測試程序:
arm
-linux
-gnueabihf
-gcc noblockioApp
.c
-o noblockioApp
編譯成功以后就會生成noblcokioApp 這個應用程序。
2、運行測試
將上一小節編譯出來noblockio.ko 和noblockioApp 這兩個文件拷貝到
rootfs/lib/modules/4.1.15 目錄中,重啟開發板,進入到目錄lib/modules/4.1.15 中,輸入如下命令
加載blockio.ko 驅動模塊:
depmod
modprobe noblockio
.ko
驅動加載成功以后使用如下命令打開noblockioApp 這個測試APP,并且以后臺模式運行:
./noblockioApp
/dev
/noblockio
&
按下開發板上的KEY0 按鍵,結果如圖52.3.3.1 所示:
當按下KEY0 按鍵以后noblockioApp 這個測試APP 就會打印出按鍵值。輸入“top”命令,查看noblockioAPP 這個應用APP 的CPU 使用率,如圖52.3.3.2 所示:
從圖52.3.3.2 可以看出,采用非阻塞方式讀處理以后,noblockioApp 的CPU 占用率也低至0.0%,和圖52.2.3.2 中的blockioApp 一樣,這里的0.0%并不是說noblockioApp 這個應用程序不使用CPU 了,只是因為使用率太小了,而圖中只能顯示出小數點后一位,因此就顯示成了0.0%。
如果要“殺掉”處于后臺運行模式的noblockioApp 這個應用程序,可以參考52.2.3 小節講解的方法。
總結
以上是生活随笔為你收集整理的Linux 阻塞和非阻塞IO 实验的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。