設備驅動中的異步通知與異步I/O

在設備驅動中使用異步通知可以使得再進行對設備的訪問時,由驅動主動通知用戶程序進行訪問。這樣,使用非阻塞I/O的應用程序無需輪詢機制查詢設備是否可訪問,而阻塞訪問也可以被類似“中斷”的異步通知所取代。除了異步通知以爲,應用還可以在發起I/O請求後,立即返回。之後,在查詢I/O完成情況,或者I/O完成後被返回。這個過程爲異步I/O。

阻塞與非阻塞訪問、poll函數提供了較好的解決設備訪問機制,但是如果有了異步通知,整套機制則更加完整了。異步通知的意思是:一旦設備就緒,則主動通知用戶程序,這樣用戶程序就不需要查詢設備狀態,這一點非常類似於硬件上的“中斷”的概念,比較準確的稱呼是“信號驅動的異步I/O”。信號是在軟件層次上對終端機制的一種模擬,在原理上,一個進程手收到一個信號與處理器收到一箇中斷請求可以說是一樣的。信號是異步的,一個進程不需要任何操作來等待信號的到達,事實上,進程也不知道信號到底什麼時候到達。

阻塞I/O意味着一直等待設備可訪問後訪問,非阻塞I/O中使用了poll函數意味着查詢設備是或否可訪問,而異步通知則意味着設備通知用戶程序自身可訪問,之後用戶在進行I/O處理。由此可見,這幾種I/O方式可以相互補充。阻塞、非阻塞、異步通知本身沒有優劣,應根據不同的應運場景合理選擇。

異步通知使用信號來實現,信號是進程間通信(IPC)的一種機制,linux可用的信號有30多種,可以百度查詢。除了SIGSYOP與SIGKILL兩個信號外,進程能夠忽略或獲取其他全部信號。一個信號被捕獲的意識是當一個信號到達是有相應的代碼處理它。如果一個信號沒有被這個進程捕獲,內核將採取默認行爲處理。

信號的接收函數

void (*singal(int signum,void(* handler))(int))(int);
//分解爲
typedef void (* sighsndler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
//第一個參數是指定信號的值,第二個參數時指定針對前面信號值的處理函數,若爲SIG_IGN,表示忽略給信號,若爲SIG_DFL,表示使用
系統默認的方式處理信號,若爲用戶自定義的函數,則信號被捕獲後,該函數將被執行。若signal()函數調用成功,他返回最後一次爲
信號signum綁定的處理函數的handler值,失敗返回SIG_ERR。

獲取“Ctrl+C”並打印相應信號值

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void sigtem_handler(int signo)
{
    printf("Have change sig N.O %d\n",signo);
    exit(0);
}

int main()
{
    signal(SIGINT,sigtem_handler);
 //   signal(SIGTERM,sigtem_handler);
    while(1);
    return 0;
}

除了signal函數外,sigaction()函數可以用於改變進程接收到信號後的行爲,它的原型爲:

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
//第一個參數爲信號的值,可以是除卻SIGKILL及SIGSTOP外的任何一個特定有效的信號。第二個參數是指向結構體sigaction的一個實例的指針,
在結構體sigaction的實例中,指定了對特定信號的處理函數,若爲空,則進程會以缺省方式對信號處理;第三個參數oldcat 指向的對象用來
保存原來對相應信號的處理函數,可指定oldact爲NULL。如果把第二、第三個參數都設置爲NULL,那麼剛函數可以用來檢測信號的有效性。

爲了使設備支持異步通知機制,驅動程序中涉及3項工作

  1. 支持F_SETOWN命令,能在這個控制命令處理中設置filp->f_owner爲對應進程ID。不過西鄉工程已有內核完成,設備驅動無需處理。、
  2. 支持F_SETFL命令的處理,每當FASYNC標誌改變時,驅動程序中的fasync()函數將得以執行。因此,驅動中應該實現fasync()函數。
  3. 在設備資源可獲得時,調用kill_fasync()函數激發相應的信號。

設備驅動中異步通知編程比較簡單,主要用到一項數據結構和兩個函數,數據結構是fasync_struct結構體,兩個函數分別是:

//處理FASYNC標誌變更的函數
int fasync_helper(int fd,struct file *filp,int mode,struct fasync_struct **fa);
//釋放信號用的函數
void kill_fasync(struct fasync_struct **fa,int sig,int band);

 異步通知函數模板

1.在設備結構體中添加異步結構體
struct xxx_dev{
    struct cdev cdve;
    ...
    struct fasync_struct *async_queue; //異步通知結構體
};
2.file_operations結構體中添加fasync
static struct file_operations xxx_fops{
    ...
    .fasync = xxx_fasync,
};
3.設備驅動fasync()函數模板
static int xxx_fasync(int fd,struct file *filp,int mode)
{
    struct xxx_dev *dev = filp->private_data;
    return fasync_helper(fd,filp,mode,&dev->async_queue);
} //即只需要把 異步結構體 作爲fasync_helper的第四個參數返回即可
4.在write函數中添加 異步信號的讀取
static ssize_t xxx_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos)
{
    struct xxx_dev *dev = filp->private_data;
    ...
    if(dev->async_queue) //產生異步讀信號
        kill_fasync(&dev->async_queue,SIGIO,POLL_IN);//調用kill_fasync 釋放信號
}
5.在文件關閉,即release函數中應用函數fasync將文件從異步通信列表中刪除
static int xxx_release(struct inode *inode,struct file *filp)
{
    xxx_fasync(-1,filp,0);
    ...
    return 0;
}

 修改阻塞I/O博文中的驅動代碼,編譯加載,測試代碼

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

static void signalio_handler(int signum)
{
    printf("receive a signal from mymodules,signalnum:%d\n",signum);
}

int main()
{
    int fd,oflags;
    fd = open("/dev/mymodules",O_RDWR,S_IRUSR | S_IWUSR);
    if(fd != -1){
        signal(SIGIO,signalio_handler);
        fcntl(fd,F_SETOWN,getpid());
        oflags = fcntl(fd,F_GETFL);
        fcntl(fd,F_SETFL,oflags | FASYNC);
        while(1){
            sleep(100);
        }
    }else
        printf("open device error\n");
    return 0;
}

編譯後測試

下面理解一下Linux的異步I/O:AIO

linux中做常用的輸入輸出(I/O)模型是同步I/O。在這個模型中,當請求發出後,應用程序就會阻塞,直到請求滿足爲止。這是一種很好的解決方案,調用應用程序在等待I/O請求完成是不需要佔用CPU資源。但是在許多應用場=場景中,I/O請求可能需要與CPU消耗產生交疊,已充分利用CPU和I/O提高吞吐率。

linux的AIO有多種實現方式,其中一種是在用戶空間的glibc庫中實現,它本質上是借用了多線程模型,用開啓的線程以同步的方法來做I/O,新的AIO輔助線程與發起AIO的線程以pthread_cond_signal()的形式進行線程間的同步。glibc的AIO主要包含如下函數:

//請求對一個有效文件描述符進行異步讀操作。
int aio_read(struct aiocb *aiocbp); //在請求進行排隊之後立即返回(儘管讀操作並未完成)。如果執行成功,返回值爲0,失敗返回-1,並設置errno的值,aiocb結構體包含了傳輸的所有信息。

//請求一個異步寫操作
int aio_write(struct aiocb *aiocbp);//請求排隊候立即返回,成功返回0,失敗返回-1.並設置errno。

//確定請求的狀態
int aio_error(struct aiocb *aiocbp); //返回 EINPROGRESS 說明尚未完成, ECANCELED 說明請求被應用程序取消了,-1 說明發生了錯誤,具體原因有errno記錄。

//獲取I/O操作的返回狀態
ssize_t aio_return(struct aiocb *aiocbp); //只有在調用 aio_error 確定請求已經完成(可能完成,可能發生錯誤)之後,擦會調用,aio_return 的返回值就等價於同步情況的read()或write()系統調用的返回值(所傳輸的字節數如果發生錯誤,返回值爲負數)。

//阻塞調用進程,知道異步請求完成爲止
int aio_suspend(const struct aiocb *const cblistp[],int n,const struct timespec *timeout);

//允許用戶取消對某個文件描述符執行的一個或所有I/O請求
int aio_cancel(int fd,struct aiocb *aiocbp);//如果請求被成功取消,返回 AIO_CANCLED 失敗返回 AIO_NOTCANCELED。

//用於同步發起多個傳輸
int lio_listo(int mode,struct aiocb *list[],int nent,struct sigeven *sig);//參數mode可以是 LIO_WAIT 或 LIO_NOWAIT,前者阻塞這個調用,知道所有的I/O完成爲止。後者I/O進行排隊之後,函數就會返回。list是一個aiocb引用的列表,最大元素個數由nent定義的。如果list的元素爲NULL,lio_listio()會將其忽略。

 AIO glibc的測試例子

#include <stdio.h>
#include <aio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define BUFSIZE 20

int main()
{
    int fd,ret;
    struct aiocb my_aiocb;

    fd = open("./module_test_pthread.c",O_RDONLY);
    if(fd < 0)
        perror("open");
    bzero(&my_aiocb,sizeof(struct aiocb));

    my_aiocb.aio_buf = malloc(BUFSIZE + 1);
    if(!my_aiocb.aio_buf)
        perror("malloc");

    my_aiocb.aio_fildes = fd;
    my_aiocb.aio_nbytes = BUFSIZE;
    my_aiocb.aio_offset = 0;

    ret = aio_read(&my_aiocb);
    if(ret < 0)
        perror("aio_read");
    while(aio_error(&my_aiocb) == EINPROGRESS)
        continue;

    if((ret = aio_return(&my_aiocb)) > 0)
        printf("ok\n");
    else
        printf("error\n");
    
    printf("Hello world\n");
    return 0;
}

 使用man_read 查看用法

 使用 gcc test.c -lrt 編譯。

 Linux AIO 也可以由內核空間實現。對於塊設備而言,AIO可以一次性發出大量的read/write 調用並通過塊層的I/O 調度來獲得更好的性能,用戶程序也可以減少過多的同步負載,還可以在業務邏輯控制中更賤靈活的進行併發控制和負載均衡。相較於glibc 的用戶空間多線程同步等實現減少了線程的負載和上下文切換等。對於網絡設備而言,在socket層面上,也可以使用AIO,讓CPU和網卡的收發動作充分交疊以改善吞吐性能。

在用戶空間,一般結合libaio來進行內核AIO的系統調用。內核AIO提供的系統調用主要包括:

int io_setup(int maxevents, io_context_t *ctxp); 
int io_destroy(io_context_t ctx); 
int io_submit(io_context_t ctx, long nr, struct iocb *ios[]); 
int io_cancel(io_context_t ctx, struct iocb *iocb, struct io_event *evt); 
int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events,    struct timespec *timeout); 
void io_set_callback(struct iocb *iocb, io_callback_t cb); 
void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset); 
void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset); 
void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,    long long offset); 
void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt,    long long offset);

AIO的讀寫請求都用io_submit() 下發。下發前通過io_prep_pwrite() 和 io_prep_pread() 生成 iocb 的結構體,作爲io_submit() 的參數。這個結構體指定了讀寫類型、起始地址、長度和標誌符的信息。讀寫請求下發之後,使用io_getevents() 函數等待I/O完成事件。io_set_callback() 則可設置一個AIO完成回調函數。

用戶空間調用io_submit() 後,對應於用戶傳遞的每一個iocb 結構,內核會產生一個與之對應的kiocb結構。file_operations包含3個與AIO相關的成員函數:

ssize_t (*aio_read) (struct kiocb *iocb, const struct iovec *iov, unsigned long    nr_segs, loff_t pos); 
ssize_t (*aio_write) (struct kiocb *iocb, const struct iovec *iov, unsigned    long nr_segs, loff_t pos); 
int (*aio_fsync) (struct kiocb *iocb, int datasync);

AIO一般由內核空間的通用代碼處理,對於塊設備和網絡設備而言,一般linux核心層的代碼已經解決。字符設備驅動一般不需要實現AIO支持。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章