你真的懂Linux內核中的阻塞和異步通知機制嗎?

@[TOC]node

阻塞/非阻塞簡介

  阻塞操做是指在執行設備操做時,若不能得到資源,則掛起進程直到知足可操做的條件後再進行操做。被掛起的進程進入睡眠狀態,被從調度器的運行隊列移走,直到等待的條件被知足。而非阻塞操做的進程在不能進行設備操做時,並不掛起,它要麼放棄,要麼不停地查詢,直至能夠進行操做爲止。編程

阻塞/非阻塞例程

  阻塞方式api

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打開 */
ret = read(fd, &data, sizeof(data)); /* 讀取數據 */

  非阻塞方式數組

int fd;
int data = 0; 
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打開 */ 
ret = read(fd, &data, sizeof(data)); /* 讀取數據 */

等待隊列簡介

  等待隊列是內核中一個重要的數據結構。阻塞方式訪問設備時,若是設備不可操做,那麼進程就會進入休眠狀態。等待隊列就是來完成進程休眠操做的一種數據結構。服務器

等待隊列相關函數

定義等待隊列

wait_queue_head_t my_queue;

  wait_queue_head_t是__wait_queue_head結構體的一個typedef。網絡

初始化等待隊列頭

void init_waitqueue_head(wait_queue_head_t *q)

  參數q就是要初始化的等待隊列頭,也可使用宏 DECLARE_WAIT_QUEUE_HEAD (name)來一次性完成等待隊列頭的定義的初始化。數據結構

定義並初始化一個等待隊列項

DECLARE_WAITQUEUE(name, tsk)

  name就是等待隊列項的名字,tsk表示這個等待隊列項屬於哪一個任務進程,通常設置爲current,在 Linux內核中 current至關於一個全局變量,表示當前進程。所以宏DECLARE_WAITQUEUE就是給當前正在運行的進程建立並初始化了一個等待隊列項。併發

將隊列項添加到等待隊列頭

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

  q:等待隊列項要加入的等待隊列頭
  wait:要加入的等待隊列項
  返回值:無app

將隊列項從等待隊列頭移除

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

  q:要刪除的等待隊列項所處的等待隊列頭
  wait:要刪除的等待隊列項。
  返回值:無異步

等待喚醒

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狀態的進程

等待事件

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函數來處理輪詢操做。當應用程序在上層經過poll,epoll,select函數來查詢設備時,驅動程序中的poll,epoll,select函數就要在底層實現查詢,若是能夠操做的話,就會從讀取設備的數據或者向設備寫入數據。

select

  函數原型

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

  nfds:要操做的文件描述符個數。
  readifds、 writefds和 exceptfds:這三個指針指向描述符集合,這三個參數指明瞭關心哪些描述符、須要知足哪些條件等等,這三個參數都是fd_set類型的, fd_set類型變量的每個位都表明了一個文件描述符。 readfds用於監視指定描述符集的讀變化,也就是監視這些文件是否能夠讀取,只要這些集合裏面有一個文件能夠讀取,那麼 seclect就會返回一個大於0的值表示文件能夠讀取。若是沒有文件能夠讀取,那麼就會根據 timeout參數來判斷是否超時。能夠將 reads設置爲NULL,表示不關心任何文件的讀變化。 writefds和 reads相似,只是 writers用於監視這些文件是否能夠進行寫操做。 exceptfds用於監視這些文件的異常
  timeout:超時時間,當咱們調用 select函數等待某些文件描述符能夠設置超時時間,超時時間使用結構體 timeval表示,結構體定義以下所示:

struct timeval { 
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */ 
};

  當 timeout爲NULL的時候就表示無限期的等待返回值。0,表示的話就表示超時發生,可是沒有任何文件描述符能夠進行操做;-1,發生錯誤;其餘值,能夠進行操做的文件描述符個數。
  操做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_set的某個位是否置1,也就是判斷某個文件是否能夠進行操做,參數fd就是要判斷的文件描述符。

void main(void) 
{  int ret, fd; /* 要監視的文件描述符 */ 
    fd_set readfds; /* 讀操做文件描述符集 */
    struct timeval timeout; /* 超時結構體 */ 
    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */ 
    FD_ZERO(&readfds); /* 清除readfds */ 
    FD_SET(fd, &readfds); /* 將fd添加到readfds裏面 */ 
     /* 構造超時時間 */ 
     timeout.tv_sec = 0; 
     timeout.tv_usec = 500000; /* 500ms */ 
     ret = select(fd + 1, &readfds, NULL, NULL, &timeout); 
     switch (ret) { 
        case 0: /* 超時 */ 
            printf("timeout!\r\n");
            break; 
        case -1: /* 錯誤 */ 
            printf("error!\r\n"); 
            break; 
        default: /* 能夠讀取數據 */ 
        if(FD_ISSET(fd, &readfds))   /* 判斷是否爲fd文件描述符 */ 
          {
               /* 使用read函數讀取數據 */ 
          } 
         break; 
    } 
 }

poll

  在單個線程中, select函數可以監視的文件描述符數量有最大的限制,通常爲1024,能夠修改內核將監視的文件描述符數量改大,可是這樣會下降效率!這個時候就可使用poll函數, poll函數本質上和 select沒有太大的差異,可是poll函數沒有最大文件描述符限制,Linx應用程序中poll函數原型以下所示:

int poll(struct pollfd *fds, nfds_t nfds, int timeout)

  函數參數和返回值含義以下
  fds:要監視的文件描述符集合以及要監視的事件,爲一個數組,數組元素都是結構體 polled類型的, pollfd結構體以下所示

struct pollfd 
{ 
    int fd; /* 文件描述符 文件描述符 文件描述符 */ 
    short events; /* 請求的事件 請求的事件 請求的事件 */
    short revents; /* 返回的事件 返回的事件 返回的事件 */ 
};

  fd是要監視的文件描述符,若是f無效的話那麼 events監視事件也就無效,而且 revents返回0。 events是要監視的事件,可監視的事件類型以下所示

POLLIN        //有數據能夠讀取。
POLLPRI        //有緊急的數據須要讀取。
POLLOUT        //能夠寫數據POLLERR指定的文件描述符發生錯誤POLLHUP指定的文件描述符掛起POLLNVAL無效的請求POLLRDNORM等同於 POLLIN

  revents:返回參數,也就是返回的事件,有Linux內核設置具體的返回事件。
  nfds:poll函數要監視的文件描述符數量
  timeout:超時時間,單位爲ms
  返回值:返回 revents域中不爲0的 polled結構體個數,也就是發生事件或錯誤的文件描述符數量;0,超時;-1,發生錯誤,而且設置errno爲錯誤類型

void main(void)
{ 
   int ret; 
   int fd; /* 要監視的文件描述符 */
   struct pollfd fds;
   fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */ 
    /* 構造結構體 */ 
   fds.fd = fd; 
   fds.events = POLLIN; /* 監視數據是否能夠讀取 */
   ret = poll(&fds, 1, 500); /* 輪詢文件是否可操做,超時500ms */ 
    if (ret)
     { /* 數據有效 */ 
      /* 讀取數據 */ 
     } else if (ret == 0) 
     { 
         /* 超時 */ 
     } else if (ret < 0) 
     {
          /* 錯誤 */ 
     } 
 }

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函數向其中添加要監視的文件描述符以及監視的事ct函數原型以下所示

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

  函數參數和返回值含義以下
  epfd;要操做的epoll句柄,也就是使用 epoll_create函數建立的epoll句柄。
  p:表示要對epfd( epoll句柄)進行的操做,能夠設置爲

EPOLL CTL ADD        //向印fd添加文件參數d表示的描述符EPOLL CTL MOD修改參數fd的 event事件。
EPOLL CTL DEL        //從f中刪除過l描述符

  fd:要監視的文件描述
  event:要監視的事件類型,爲 epoll_event結構體類型指針, epoll_event結構體類型以下所

struct epoll_event 
{
     uint32_t events; /* epoll事件 */ 
    epoll_data_t data; /* 用戶數據 用戶數據 */ 
};

  結構體 epoll_event的 events成員變量表示要監視的事件,可選的事件以下所示

EPOLLIN            //有數據能夠讀取EPOLLOUT能夠寫數據
EPOLLPRI        //有緊急的數據須要讀取EPOLLERI指定的文件描述符發生錯誤。
EPOLLHUP        //指定的文件描述符掛起POLLET設置epo爲邊沿觸發,默認觸發模式爲水平觸發王
POLLONESHOT        //一次性的監視,當監視完成之後還須要再次監視某個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結構體的數組,當有事件發生的時候Iimx內核會填寫 events,調用者能夠根據 events判斷髮生了哪些事件。
  prevents:events數組大小,必須大於0
  timeout:超時時間,單位爲ms返回值:0,超時;-1,錯誤;其餘值,準備就緒的文件描述符數量。
  epoll更多的是用在大規模的併發服務器上,由於在這種場合下 select和poll並不適合。當設計到的文件描述符(fd比較少的時候就適合用 selcet和pl本章咱們就使用 sellect和poll這兩個函數

異步通知概念

  阻塞與非阻塞訪問、poll函數提供了較好的解決設備訪問的機制,可是若是有了異步通知,整套機制則更加完整了。
  異步通知的意思是:一旦設備就緒,則主動通知應用程序,這樣應用程序根本就不須要查詢設備狀態,這一點很是相似於硬件上「中斷」的概念,比較準確的稱謂是「信號驅動的異步I/O」。信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。信號是異步的,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。
  阻塞I/O意味着一直等待設備可訪問後再訪問,非阻塞I/O中使用poll()意味着查詢設備是否可訪問,而異步通知則意味着設備通知用戶自身可訪問,以後用戶再進行I/O處理。因而可知,這幾種I/O方式能夠相互補充。

Linux信號

  異步通知的核心就是信號,在 arch/xtensa/include/uapi/asm/signal.h文件中定義了Linux所支持的全部信號

#define SIGHUP      1/* 終端掛起或控制進程終止 */ 
#define SIGINT      2/* 終端中斷(Ctrl+C組合鍵) */ 
#define SIGQUIT     3 /* 終端退出(Ctrl+\組合鍵) */
#define SIGILL      4/* 非法指令 */ 
#define SIGTRAP     5/* debug使用,有斷點指令產生 */
#define SIGABRT     6/* 由abort(3)發出的退出指令 */ 
#define SIGIOT      6 /* IOT指令 */ 
#define SIGBUS      7 /* 總線錯誤 */ 
#define SIGFPE      8 /* 浮點運算錯誤 */ 
#define SIGKILL     9 /* 殺死、終止進程 */ 
#define SIGUSR1     10 /* 用戶自定義信號1 */ 
#define SIGSEGV     11 /* 段違例(無效的內存段) */
#define SIGUSR2     12 /* 用戶自定義信號2 */ 
#define SIGPIPE     13 /* 向非讀管道寫入數據 */ 
#define SIGALRM     14 /* 鬧鐘 */
#define SIGTERM     15 /* 軟件終止 */
#define SIGSTKFLT   16 /* 棧異常 */
#define SIGCHLD     17 /* 子進程結束 */
#define SIGCONT     18 /* 進程繼續 */
#define SIGSTOP     19 /* 中止進程的執行,只是暫停 */
#define SIGTSTP     20 /* 中止進程的運行(Ctrl+Z組合鍵) */ 
#define SIGTTIN     21 /* 後臺進程須要從終端讀取數據 */ 
#define SIGTTOU     22 /* 後臺進程須要向終端寫數據 */
#define SIGURG      23 /* 有"緊急"數據 */
#define SIGXCPU     24 /* 超過CPU資源限制 */ 
#define SIGXFSZ     25 /* 文件大小超額 */ 
#define SIGVTALRM   26 /* 虛擬時鐘信號 */ 
#define SIGPROF     27 /* 時鐘信號描述 */
#define SIGWINCH    28 /* 窗口大小改變 */ 
#define SIGIO       29 /* 能夠進行輸入/輸出操做 */
#define SIGPOLL SIGIO 
 /* #define SIGLOS 29 */ 
#define SIGPWR      30 /* 斷點重啓 */ 
#define SIGSYS      31 /* 非法的系統調用 */ 
#define SIGUNUSED   31 /* 未使用信號 */

異步通知代碼

  咱們使用中斷的時候須要設置中斷處理函數,一樣的,若是要在應用程序中使用信號,那麼就必須設置信號所使用的信號處理函數,在應用程序中使用 signal函數來設置指定信號的處理函數, signal函數原型以下所示

void (*signal(int signum, void (*handler))(int)))(int);

  該函數原型較難理解,它能夠分解爲:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

  第一個參數指定信號的值,第二個參數指定針對前面信號值的處理函數,若爲SIG_IGN,表示忽略該信號;若爲SIG_DFL,表示採用系統默認方式處理信號;若爲用戶自定義的函數,則信號被捕獲到後,該函數將被執行。
  若是signal調用成功,它返回最後一次爲信號signum綁定的處理函數的handler值,失敗則返回SIG_ERR。

驅動中的信號處理

fasync_struct結構體

  首先咱們須要在驅動程序中定義個 fasync_struct結構體指針變量, fasync_struct結構體內容以下

struct fasync_struct 
{ spinlock_t fa_lock; 
int magic; 
int fa_fd; 
struct fasync_struct *fa_next; 
struct file *fa_file; 
struct rcu_head fa_rcu; 
};

  通常將 fasync_struct結構體指針變量定義到設備結構體中,好比在xxx_dev結構體中添加一個 fasync_struct結構體指針變量,結果以下所示

struct xxx_dev 
{ 
    struct device *dev; 
    struct class *cls;
    struct cdev cdev;
 ...... 
     struct fasync_struct *async_queue; /* 異步相關結構體 */
 };

fasync函數

  若是要使用異步通知,須要在設備驅動中實現file_ operations操做集中的 fasync函數,此函數格式以下所示:

int (*fasync) (int fd, struct file *filp, int on)

  fasync函數裏面通常經過調用 fasync_helper函數來初始化前面定義的 fasync_struct結構體指針, fasync_helper函數原型以下

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

  fasync_helper函數的前三個參數就是 fasync函數的那三個參數,第四個參數就是要初始化的 fasync_ struct結構體指針變量。當應用程序經過結構體指針變量。當應用程序經過「 fcntl(fd, F_SETFL, flags | FASYNC)」改變fasync標記的時候,驅動程序 file_operations操做集中的 fasync函數就會執行。

struct xxx_dev 
{ 
    ......
    struct fasync_struct *async_queue; /* 異步相關結構體 */ 
}; 
static int xxx_fasync(int fd, struct file *filp, int on)
{
     struct xxx_dev *dev = (xxx_dev)filp->private_data; 
     if (fasync_helper(fd, filp, on, &dev->async_queue) < 0) 
     return -EIO; 
     return 0; 
 } 
     static struct file_operations xxx_ops =
 { 
       ...... 
      .fasync = xxx_fasync,
      ...... 
 };

  在關閉驅動文件的時候須要在file_ operations操做集中的 release函數中釋放 fasyn_fasync struct的釋放函數一樣爲 fasync_helper, release函數參數參考實例以下

static int xxx_release(struct inode *inode, struct file *filp) 
 { 
     return xxx_fasync(-1, filp, 0); /* 刪除異步通知 */
 }
static struct file_operations xxx_ops =
 { 
    ...... 
    .release = xxx_release, 
 };

  第3行經過調用示例代碼 xxx_fasync函數來完成 fasync_struct的釋放工做,可是,其最終仍是經過 fasync_helper函數完成釋放工做。

kill_fasync函數

  當設備能夠訪問的時候,驅動程序須要嚮應用程序發出信號,至關於產生「中斷」 kill_fasync函數負責發送指定的信號, kill_fasync函數原型以下所示

void kill_fasync(struct fasync_struct **fp, int sig, int band)

  函數參數和返回值含義以下:
  fasync struct 要操做的文件指針
  sig:要發送的信號
   band:可讀時設置爲 POLL IN,可寫時設置爲 POLL OUT。
  返回值:無。

應用程序對異步通知的處理

  應用程序對異步通知的處理包括如下三步
  一、註冊信號處理函數應用程序根據驅動程序所使用的信號來設置信號的處理函數,應用程序使用 signal函數來設置信號的處理函數。前面已經詳細的講過了,這裏就不細講了。
  二、將本應用程序的進程號告訴給內核使用fcntl(fd, F_SETOWN, getpid)將本應用程序的進程號告訴給內核
  三、開啓異步通知使用以下兩行程序開啓異步通知:

flags = fcntl(fd, F_GETFL); /* 獲取當前的進程狀態*/ 
fcntl(fd, F_SETFL, flags | FASYNC); /* 開啓當前進程異步通知功能 */

  重點就是經過 fcntl函數設置進程狀態爲 FASYNC,通過這一步,驅動程序中的 fasync函數就會執行。

你們的鼓勵是我繼續創做的動力,若是以爲寫的不錯,歡迎關注,點贊,收藏,轉發,謝謝!
如遇到排版錯亂的問題,能夠經過如下連接訪問個人CSDN。

**CSDN:[CSDN搜索「嵌入式與Linux那些事」]

相關文章
相關標籤/搜索