1. 程式人生 > >libevent原始碼學習-----統一事件源及訊號繫結函式

libevent原始碼學習-----統一事件源及訊號繫結函式

libevent在對檔案描述符,套接字進行監控時直接放到event,這些event通過io多路複用函式進行監控,然而對應訊號來說io複用函式卻無能為力,為了解決問題,libevent採用統一事件源的方式,即將訊號也表現成event的形式,用到了socketpair套接字對

socketpair套接字對
套接字對也是通訊方式的一種,在程序間通訊時相比於管道和命名管道而言更簡單,也更安全
linux下使用socketpair函式建立一對套接字,函式原型

/*
 *@param d: 協議族,通常為AF_UNIX
 *@param type: 套接字型別,如SOCK_STREAM
 *@param protocol: 協議,0即可
 *@param sv: 存放建立的兩個套接字
 */
int socketpair(int d, int type, int protocol, int sv[2]);

建立的套接字對的每一端都可以進行讀寫,也就是sv[0]可以既作為讀端也可以作為寫端,sv[1]也是。
libevent中sv[1]為讀端,sv[0]為寫端。向sv[0]中寫入資料時sv[1]會變為可讀

對於訊號的處理,其實使用者可以自己使用sigaction註冊訊號處理函式然後自己提供回撥函式,但是既然想要交給libevent處理,就說明使用者只提供回撥函式,其他什麼都不管。
也可以在內部只調用sigaciton/signal註冊訊號處理函式,回撥函式是使用者提供的那個,這樣也可以滿足需求,但是既然統一事件源,就需要把訊號也當成event處理。

libevent的做法是為

  • 讀端註冊到base中,提供一個內部的回撥函式,該步驟僅僅呼叫一次
  • 訊號值作為fd建立event,繫結內部回撥函式,註冊到base中
  • 為訊號繫結一個內部的訊號處理函式,在訊號處理函式中將發生的訊號值寫入套接字對的寫端
  • 此時讀端變為可讀,呼叫這個讀端套接字繫結的回撥函式
  • 讀端回撥函式中讀取訊號值,將訊號值對應的event新增到啟用佇列中
  • 啟用佇列處理

我覺得完全可以直接在內部使用sigaction/signal繫結訊號值和使用者的訊號處理函式
但是既然libevent想要把所有東西都當成event來處理,就需要將訊號轉成event,把所有event都放到啟用佇列中一起處理

複習一下linux下的訊號處理機制

UNIX系統訊號機制最簡單的介面是signal函式,函式原型為

#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
  • signo表示使用者關心的訊號型別,如終端訊號,退出訊號,SIGINT,SIGQUIT等
  • func可以是一個函式指標,也就是訊號處理函式,當signno訊號發生時,會呼叫這個函式。該函式有一個int型別引數,通常是訊號值。也可以是常量SIG_IGN表示核心忽略該訊號(SIGKILL和SIGSTOP不能被忽略),常量SIG_DFL表示執行系統預設的處理動作,通常是終止當前程序
  • 返回值是指向在此之前的訊號處理函式的指標

signal的使用還是比較簡單的

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

void handler(int signo);

int main()
{
    signal(SIGINT,handler);
    for(;;)
        pause();
    return 0;
}

void handler(int signo)
{
    printf("receive sig=%d\n", sig);
}

程式首先註冊訊號處理函式,之後當中斷髮生後切換到核心態,在核心的中斷處理程式執行完後返回使用者態之前檢測到有訊號SIGINT發生,核心遍不恢復到main函式而是直接轉到handler函式中,在執行完handler後由核心轉到main函式中繼續執行。main和handler是兩個獨立的存在

signal並不屬於POSIX標準,在UNIX中使用sigaction函式作為signal的替代品,signaction函式的功能是檢查或修改(或檢查並修改)與制定訊號相關聯的處理動作,函式原型為

#include <signal.h>
int sigaction(int signo, const struct sigaction *act,
              struct sigaction *oact);
  • signno是要檢測或修改其處理動作的訊號編號,如SIGINT,SIGQUIT等
  • act為要修改的動作,本質上是訊號處理函式
  • oact為以前的訊號處理動作,不關心時可以設定為NULL
  • 呼叫成功返回0,失敗返回-1(linux的系統呼叫返回值大多是這樣)

結構體struct sigaction如下

struct sigaction
{
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_sigaction)(int, siginfo_t *, void *);
}
  • sa_handler和signal的第二個引數相同,表示訊號處理函式,SIG_IGN表示忽略,SIG_DFL表示預設動作,引數為訊號值
  • sa_mask是訊號遮蔽字集,如果設定,那麼在進行訊號處理函式時,如果有在sa_mask中的訊號到達,則不會通知程序,而是在訊號處理函式執行完後才通知。預設情況下,如果程式正在執行訊號處理函式,那麼對於當前這個訊號處理函式處理的訊號不會再次送到程序。
  • 核心維護著一個訊號佇列,如果當前有正在處理的訊號處理函式,會把其他到達程序的訊號放到這個佇列中。若同一種訊號多次發生,通常只會新增一次
  • sa_sigaction和sa_flags配合使用,與sa_handler相對應,通常使用sa_handler就不需要使用sa_sigaction,主要用於實時訊號可以帶資訊,使用sa_handler時sa_flags設為0即可

通常使用sa_handler和sa_mask就可以滿足需求了

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

void handler(int signo);

int main()
{
    struct sigaction act;
    act.sa_handler = handler;
    /* 清空訊號遮蔽集 */
    sigemptyset(&act.sa_mask);
    /* 在訊號處理函式執行期間遮蔽SIGQUIT,執行完後送達當前程序 */
    sigaddset(&act.sa_mask, SIGQUIT);
    act.sa_flags = 0;

    if(sigaction(SIGINT, &act, NULL) < 0)
    {
        perror("sigaction error");
        exit(1);
    }

    for(;;)
        pause();

    return 0;
}

void handler(int signo)
{
    printf("receive sig=%d\n", signo);
    sleep(5);
}

先按ctrl+c傳送中斷訊號,馬上按ctrl+z傳送終止訊號,發現程式5秒後才終止,說明sa_mask中的訊號如果發生了會在當前訊號處理函式完成後才送達當前程序