信號是什么?
信號是事件發送時對進程的一種通知機制。有時也稱之為軟件中斷。信號與硬件中斷的相似之處在于打斷了程序執行的正常流程,大多情況下,無法預測信號到達的精確時間。
一個具有合適權限的進程不僅能夠向另一進程發送信號,也可以向自身發送信號。然而,發往進程的諸多信號,通常都是源于內核。
linux 信號可由如下條件產生:
- 對于前臺進程,用戶可以通過輸入特殊的終端字符來給它發送信號。比如輸出 Ctrl+C 通常會給進程發送一個終端信號。
- 系統異常。比如浮點異常和非法訪問段內存。
- 系統狀態變化。比如 alarm 定時器到期引起的 SIGALRM 信號。
- 運行 kill 命令或者調用 kill 函數。
服務器程序必須處理(或至少忽略)一些常見的信號,以免異常終止。
signalfd 是什么?
signalfd 是一個將信號抽象的文件描述符,將信號的異步處理轉換為文件的I/O 操作。通過文件描述符就緒的方法來通知信號的到來,當有信號發生時可以對其 read,這樣可以將信號的監聽放到 select、poll、epoll 等監聽隊列中。
通過文件描述符就緒的方法來通知信號的到來,當有信號發生時可以對其read,這樣可以將信號的監聽放到 select、poll、epoll 等監聽隊列中。
signalfd 的系統調用接口
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
創建并返回一個用于所受信號的文件描述符。
mask:信號的集合,這里主要是你想監聽的信號的集合。
flags 可以使用以下標志位進行或(or)的結果:
- SFD_NONBLOCK: 文件會被設置成 O_NONBLOCK,讀操作不阻塞。若不設置,一直阻塞直到計數器中的值大于0。
- SFD_CLOEXEC: 在新的文件描述符上設置 close-on-exec ( FD_CLOEXEC ) 標志,簡單說就是 fork 子進程時不繼承。
獲取 signalfd 文件描述符后,我們來查看一下可以對其做哪些操作。
static const struct file_operations signalfd_fops = {
#ifdef CONFIG_PROC_FS
.show_fdinfo = signalfd_show_fdinfo,
#endif
.release = signalfd_release,
.poll = signalfd_poll,
.read = signalfd_read,
.llseek = noop_llseek,
};
通過上面 signalfd 實現的調用可知, 我們可以對 eventfd 進行 read、poll、close 等操作。
下面通過一個例子來了解下 signalfd 的使用方式,具體完整代碼可通過 man signalfd 獲取
int main(int argc, char *argv[])
{
...
//初始化信號集
sigemptyset(&mask);
//添加信號到信號集中
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
//將mask所指向的信號集中所包含的信號加到當前的信號掩碼中,作為新的信號屏蔽字,關閉內核的默認行為。
sigprocmask(SIG_BLOCK, &mask, NULL)
//創建 signalfd 文件描述符
sfd = signalfd(-1, &mask, 0);
for (;;) {
//阻塞等待信號發生并讀取。根據讀取的結果可以知道發生了什么信號
s = read(sfd, &fdsi, sizeof(fdsi));
if (fdsi.ssi_signo == SIGINT) {
printf("Got SIGINTn");
} else if (fdsi.ssi_signo == SIGQUIT) {
printf("Got SIGQUITn");
exit(EXIT_SUCCESS);
} else {
printf("Read unexpected signaln");
}
}
}
當沒有信號時,進程阻塞在 read 調用上,當有信號發生時,結果如下:
$ ./signalfd_demo
^C # Control-C generates SIGINT
Got SIGINT
^C
Got SIGINT
^ # Control- generates SIGQUIT
Got SIGQUIT
$
每次 Control + C,進程都會捕獲到一次信號,并打印具體信息。
通過如下查看,得到 signalfd 其實也是一個匿名 fd 類型。
[root@localhost ~]# ll /proc/48356/fd/
lrwx------ 1 root root 64 5月 23 11:54 3 ->anon_inode:[signalfd]
signalfd 源碼解析
接下來我們通過分析源碼的方式來探究 signalfd 的底層實現原理。
signalfd ( signalfd4 )
SYSCALL_DEFINE3(signalfd, int, ufd, sigset_t __user *, user_mask, size_t, sizemask)
{
...
return do_signalfd4(ufd, &mask, 0);
}
SYSCALL_DEFINE4(signalfd4, int, ufd, sigset_t __user *, user_mask, size_t, sizemask, int, flags)
{
...
return do_signalfd4(ufd, &mask, flags);
}
static int do_signalfd4(int ufd, sigset_t *mask, int flags)
{
struct signalfd_ctx *ctx;
sigdelsetmask(mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
signotset(mask);
//內核新創建signalfd
if (ufd == -1) {
//創建一個signalfd_ctx內核結構
ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
//設置信號集合
ctx->sigmask = *mask;
//獲取一個匿名句柄,file->f_op 設置為 signalfd_fops
ufd = anon_inode_getfd("[signalfd]", &signalfd_fops, ctx, O_RDWR | (flags & (O_CLOEXEC | O_NONBLOCK)));
} else { //已經創建signalfd
//合法性檢查
struct fd f = fdget(ufd);
//設置為新的值
ctx->sigmask = *mask;
//喚醒阻塞在當前進程的信號等待隊列
wake_up(¤t->sighand->signalfd_wqh);
fdput(f);
}
return ufd;
}
signalfd 的操作就是創建或者修改內核結構 signalfd_ctx,signalfd 本身也是一個匿名句柄。
對于 signalfd_ctx 內核結構,就只有一個字段,該字段記錄用戶設置的信號集合。
struct signalfd_ctx {
sigset_t sigmask;
};
signalfd_read
static ssize_t signalfd_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int nonblock = file->f_flags & O_NONBLOCK;
count /= sizeof(struct signalfd_siginfo);
//存放讀取到的數據結構
siginfo = (struct signalfd_siginfo __user *) buf;
do {
//從信號隊列中獲取一個信號,然后填充到info中
ret = signalfd_dequeue(ctx, &info, nonblock);
//把獲取到的信號填充到返回給用戶的數據結構中
ret = signalfd_copyinfo(siginfo, &info);
siginfo++;
total += ret;
nonblock = 1;
} while (--count);
return total ? total: ret;
}
static ssize_t signalfd_dequeue(struct signalfd_ctx *ctx, kernel_siginfo_t *info,
int nonblock)
{
ssize_t ret;
DECLARE_WAITQUEUE(wait, current);
spin_lock_irq(¤t->sighand->siglock);
//從掛起信號隊列中獲取信號
ret = dequeue_signal(current, &ctx->sigmask, info);
switch (ret) {
case 0: //若沒有信號,判斷是否需要阻塞
if (!nonblock)
break; //阻塞,跳出,往下走進行休眠
ret = -EAGAIN; //非阻塞,往下走到default,函數返回
default:
spin_unlock_irq(¤t->sighand->siglock);
return ret;
}
//把當前進程加入信號等待隊里中
add_wait_queue(¤t->sighand->signalfd_wqh, &wait);
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
//從掛起信號隊列中獲取信號
ret = dequeue_signal(current, &ctx->sigmask, info);
//存在信號,跳出循環,退出
if (ret != 0)
break;
//檢查當前進程是否有信號處理,返回不為0表示有信號需要處理
if (signal_pending(current)) {
ret = -ERESTARTSYS;
break;
}
spin_unlock_irq(¤t->sighand->siglock);
schedule(); //進程調度,進入休眠
spin_lock_irq(¤t->sighand->siglock);
}
spin_unlock_irq(¤t->sighand->siglock);
//把當前進程從等待隊列中刪除
remove_wait_queue(¤t->sighand->signalfd_wqh, &wait);
__set_current_state(TASK_RUNNING);
return ret;
}
signalfd 的讀操作很簡單,主要操作如下:
- 查看信號隊列中是否有信號,若有信號取出信號,并返回給用戶。
- 若句柄是阻塞類型的,在沒有信號的情況下,則進程進入休眠,直到有信號到來。
- 若句柄是非阻塞類型的,則直接返回 EAGAIN。
signalfd_poll
static __poll_t signalfd_poll(struct file *file, poll_table *wait)
{
struct signalfd_ctx *ctx = file->private_data;
__poll_t events = 0;
//把一個wait等待隊列掛到當前進程的信號等待隊列signalfd_wqh,其回調函數為ep_poll_callback
poll_wait(file, ¤t->sighand->signalfd_wqh, wait);
...
return events;
}
該函數的操作就是把 wait對象直接掛到當前進程的信號等待隊列signalfd_wqh 中,對比 timerfd 來講,區別在于 timerfd 的 wait 對象是掛到 timerfd_ctx->wqh 鏈表中。(詳情參看定時器timerfd原理)
signalfd 與 epoll 的結合
- 當 epoll_ctl 調用 signalfd_poll 時,會把生成的 wait 對象掛到當前進程的信號等待隊列 signalfd_wqh 中,其中 wait 的回調函數為ep_poll_callback 。
- 當觸發信號時,內核會遍歷 signalfd_wqh 上的 wait 對象,然后調用回調函數 ep_poll_callback,在該回調函數中會把觸發的事件發送到用戶態,然后喚醒由于調用 epoll_wait 而休眠的進程,喚醒后的進程調調用 ead 去取信號。