信號
目錄- 信號
- 基礎和概念
- 信號處置
- 信號處理器
- 改變信號處置:signal()
- 改變信號處置:sigaction()
- struct sigaction
- 信號資訊的攜帶:siginfo_t
- 父子信號處理
- 信號發送
- 發送信號:kill()
- 信號發送的權限
- 舉例
- 向自己發送信號:raise()
- sigqueue()
- 行程組通知:killpg()
- 等待信號:pause()
- 顯示信號資訊:strsignal()
- 發送信號:kill()
- 信號集
- 初始化信號集
- 操作信號集
- GNU C的拓展
- 信號掩碼
- 信號傳遞的阻塞:
- sigprocmask函式
- 信號處置
- 信號處理器
- 概念
- 可重入函式
- 異步信號安全函式
- 概念
- 計時器與休眠
- 計時器
- 間隔計時器
- 休眠
- 高精度
- 計時器
- 基礎和概念
總結自Unix手冊第20 21 22章
信號產生的程序:信號因某事件而產生,稍后(信號的產生和傳遞之間存在時間間隔,這個時間間隔可能是因為行程正在執行某個系統呼叫,因此在這個系統呼叫回傳前,信號不會被傳遞,此時信號處于等待(pending狀態)被傳遞至指定行程,行程接收信號后作出回應,
基礎和概念
信號處置
信號處理器
信號處理器:信號被捕獲時呼叫的函式,該函式由內核代表行程進行呼叫,保證可以隨時打斷接收信號的行程,信號處理器的設計應該力求簡單,信號處理器形如
void handler(int sig)
{
}
傳入信號的編號,處理器可以根據信號種類的不同選擇性的執行一些代碼,也就是說,一個信號處理器可以用來處理多種不同的信號,
改變信號處置:signal()
signal函式不如sigaction函式優秀,前者在不同UNIX實作中存在差異,但后者使用更加復雜(功能也更強大)
#include <signal.h>
sig_t signal(int sig, sig_t handler)
- sig希望改變處理行為的信號編號
- handler指明改變后信號處理函式,一般這個函式具有這樣的形式
改變信號處置:sigaction()
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
sig信號const struct sigaction *act:是一個指標,指向描述信號新處置方式的資料結構,具體使用見下struct sigaction *oldact:回傳之前信號處置的資訊
struct sigaction
struct sigaction
{
union
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t * , void * );
} sigaction_handler;
// 信號的集合,用于記錄被阻塞的信號 需要用特定函式處理
sigset_t sa_mask;
// 指明信號處理的行為
int sa_flags;
void (*sa_restorer) (void);
};
sigaction_handler:(匿名聯合體,存在于結構體或聯合體中,使用時不需要通過聯合體名,可以直接使用)sa_handler為函式指標,對應signal()中的handler函式- 可以指向
SIG_DEL,SIG_IGN常量之一 - 可以自定信號處理器
- 只有使用自定義函式,
sa_mask和sa_flags的設定才有意義
- 只有使用自定義函式,
- 可以指向
sa_sigaction函式指標,可以完成復雜的信號作業
sa_mask用于設定信號處理器執行時阻塞的信號,- 使用細節是:內核呼叫信號處理器之前,將
sa_mask中設定掩碼的信號添加到行程的信號掩碼中,直至信號處理器函式回傳,再從行程的信號掩碼中移除之前添加的信號,
- 使用細節是:內核呼叫信號處理器之前,將
sa_flags是位掩碼,設定需要使用|SA_SIGINFO- 發送信號時發送附加資訊,信號處理器要宣告成
void handler(int sig, siginfo_t *info, void *context);
- 發送信號時發送附加資訊,信號處理器要宣告成
sa_restorer沒看到如何用,空下
信號資訊的攜帶:siginfo_t
一個非常復雜的結構體,未了解,
父子信號處理
父行程創建子行程,子行程繼承父行程信號處理方式,直到子行程呼叫exec函式,exec函式將呼叫者的信號處理方式還原成默認,
信號發送
發送信號:kill()
不是去扼殺行程,而是只發送信號,只是早期UNIX實作中大多數信號的功能是終止行程,
#include <signal.h>
int kill(pid_t pid, int sig);
pid標識一個或多個目標行程pid > 0:pid為指定行程pid == 0:pid發送信號給發送信號所在的行程同組的每個行程- 也就是所有子行程
pid < -1:向組ID為-pid的行程組內所有下屬行程pid == -1:呼叫行程有權發送的所有行程(如果使用ssh連接服務器實驗,執行后發現ssh斷了)
信號發送的權限
發送信號必須滿足發送信號的行程和接收信號的行程的用戶ID相同,或者是發送信號的行程的用戶是root,
舉例
// 這個是發送信號的代碼
#include <sys/types.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
int result, i;
if(argc > 1)
{
result = kill(atoi(argv[1]), SIGINT);
printf("result = %d\n", result);
}
return 0;
}
// 這個是接收信號的代碼
// 只是一個耗時間的計算,注意不能用sleep代替,因為sleep會使行程掛起
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("%d\n", getpid());// 輸出此行程的pid
int i = 0;
double x = 5, y = 0.9548, e = 2.7;
for(i = 0; i < 500000000; i++)
{
x = x * e + (x - i) * e - 3.65 * (y - e);
y = y * (x - e);
x -= y;
}
printf("%f\n", x);
return 0;
}
// 測驗kill(0, SIGINT);
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig);
int main(int argc, char **argv)
{
signal(SIGINT, handler);
pid_t father = getpid(), son[10];
for (size_t i = 0; i < 3; i++)
{
if (getpid() == father)
{
son[i] = fork();
if (son[i] != 0)
{
printf("when %ld : %d -> %d\n", i, father, son[i]);
}
else
{
sleep(2);// 子行程休眠,等待父行程發送信號
printf("%d is safe\n", getpid());
}
}
}
if (getpid() == father)
{
sleep(1); // 如果不加這句話,可能子行程被創建但沒來得及執行就被kill了資訊
kill(0, SIGINT);
printf("%d is safe\n", getpid());
sleep(2);
}
return 0;
}
void handler(int sig)
{
printf("%d use handler SIGINT\n", getpid());
}
向自己發送信號:raise()
int raise(int sig);
- 對于單執行緒:相當于
kill(getpid(), sig); - 對于非單執行緒:相當于
pthread_kill(pthread_self(), sig);
由于信號的處理有內核呼叫信號處理器完成,所以行程使用raise時,信號立即傳遞并被處理,甚至在raise呼叫回傳前,raise函式呼叫成功回傳0,失敗(唯一的失敗是EINVAL,sig無效)回傳非0值
/**
*
* 所以這個函式一定是先輸出handle done
* 再輸出result
*
*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
printf("handle done\n");
}
int main(int argc, char **argv)
{
signal(SIGINT, handler);
int result = raise(SIGINT);
printf("%d\n", result);
return 0;
}
sigqueue()
向行程發送信號,沒用過也沒實驗過
int sigqueue(pid_t pid, int sig, const union sigval value);
行程組通知:killpg()
#include <signal.h>
int killpg(pid_t pgrp, int sig)
向某一行程組的所有成員發送信號,相當于kill(-pgrp, sig);
等待信號:pause()
暫停行程執行,知道信號處理器被呼叫,中斷pause,被中斷程式回傳-1,并設定errno,
#include <unistd.h>
int pause();
顯示信號資訊:strsignal()
有三種顯示的方式
sys_siglist陣列,使用sys_siglist[SIGXXX]獲得信號的描述strsignal()函式,回傳信號描述的字串,推薦使用strsignal()函式,因為會有安全的邊界檢查,而且該函式設定了地區敏感,可以顯示本地語言(沒感覺出來)psignal()函式,在標準錯誤設備上輸出msg資訊和sig的描述
#define _BSD_SOURCE
// 5.4.0-70-generic下使用這個紅出現了警告
// # warning "_BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE"
#include <signal.h>
extern const char *const sys_siglist[];
#define _GNU_SOURCE
#include <string.h>
char *strsignal(int sig)
#include <signal.h>
void psignal(int sig, const char *msg);
信號集
用于表述多個信號的資料結構,sigset_t,這在Linux上以為掩碼形式存在,
初始化信號集
sigemptyset()以空的形式初始化信號集,sigfillset()以填充所有信號的形式初始化,SUSv3只要求對sigset_t賦值即可,sigset_t其實可以用手動賦值,但這樣有損于可移植性,而Linux使用函式實作,增強了可移植性,
#include <signal.h>
int sigemptyset(sigset_t *__set)
int sigfillset(sigset_t *__set)
操作信號集
分別有從信號集中添加和刪去某個信號,或是判斷這個信號是否在信號集中
#include <signal.h>
int sigaddset(sigset_t *set, int sig)
int sigdelset(sigset_t *set, int sig)
int sigismember(sigset_t *set, int sig);
int sigpending(sigset_t *set);
sigismember:sig存在于set中回傳1,否則回傳0sigpending:回傳呼叫行程處于等待的信號
GNU C的拓展
需要在宏中添加
#define _GNU_SOURCE
具體的三個函式這里沒有寫
信號掩碼
信號傳遞的阻塞:
每個行程擁有一個信號掩碼,由內核維護,記錄著需要阻塞的信號,如果內核發送該行程的信號掩碼中記錄的信號給該行程,那么這個信號會被阻塞,除非從行程掩碼中移除,更進一步,信號掩碼可以細致到執行緒級別
sigprocmask函式
- 修改該行程的信號掩碼
- 獲得該行程的信號掩碼
- 設定
set為NULL且how為SIG_BLOCK,可以用oldset中獲得信號掩碼
- 設定
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oldset)
how指定了函式的具體行為SIG_BLOCK,將set信號集中的信號添加到信號掩碼中SIG_UNBLOCK,將set中的信號從掩碼中移除SIG_SETMASK,將set信號集賦值給掩碼
set指定的程式需要處理的信號的信號集oldset得到修改之前的信號掩碼
信號處理器
概念
信號處理器和主程式是兩條獨立的執行緒,同屬于同一行程,
可重入函式
同一行程多個執行緒看同時安全(產生預期的結果)呼叫的函式
要求
- 只是用本地變數
不可重入函式的特點
- 使用全域變數和靜態資料結構可能是不可重入
- 使用靜態分配的記憶體,這次呼叫會覆寫上次呼叫的資訊
常見不可重入函式舉例
- 不可重入
- malloc函式族
異步信號安全函式
可重入或信號處理器函式無法中斷的函式
計時器與休眠
定時器精度問題:沒有寫,見UNIX編程手冊23.2章和10.6章
計時器
linux對每個行程設定3個計時器計時器的種類有:
| 真實計時器 | 虛擬計時器 | 實用計時器 | |
|---|---|---|---|
| C語言中的值 | ITIMER_REAL |
ITIMER_VIRTUAL |
ITIMER_PROF |
| 記錄時間 | 程式運行的總時間 | 在用戶態的時間之和 | 在用戶態和內核態的時間之和 |
| 到期發送信號 | SIGALRM |
SIGVTALRM |
SIGPROF |
對這些信號的默認處理是終止行程,除非自定義信號處理函式,
間隔計時器
計時器資料結構:
struct itimerval
{
struct timeval it_interval;
struct timeval it_value;
};
struct timeval // 時間的資料結構
{
long tv_sec; // Seconds
long tv_usec; // Microseconds
};
系統使用settimer創建定時器
#include <sys/times.h>
int setitimer(int which, const struct itimerval *new, struct itimerval *old)
which指明需要創建哪種計時器- 使用C語言預定義值指明
newit_value指明定時器到期的計時時間- 兩個值都為
0表示屏蔽計時器 - 值表示初始的間隔時間,即第一次發送信號的時間間隔
- 兩個值都為
it_interval指明定時器是否是周期性定時器- 為
0時不表示間隔時間是0,而是表示計時器不是周期性的,是一次性的 - 不為
0時表示計時value后每次間隔interval再發送信號
- 為
oldold不為NULL時,則該值指向函式設定計時器時的前一個設定,用于計時器設定的還原
函式行為:計時器會從new.it_value開始倒計時直到0為止,遞減至0時發送信號,若new.it_interval != 0,重置并開始計時
一個行程只能擁有三種計時器的一種,所以之后再次呼叫setitimer時會修改上一次的設定值
#include <sys/times.h>
int getitimer(int which, struct itimerval *value)
獲得當前計時器的狀態,類似于setitimer中的old
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
設定一個sedonds秒后到期的計時器,到期時發送SIGALRM信號,(這也會覆寫之前的設定,alarm(0)表示屏蔽所有計時器)
休眠
#include <unistd.h>
unsigned int sleep(unsigned int seconds); // 休眠seconds秒
void usleep(unsigned long usec); // 休眠usec * 10 ^ -6秒
// 這兩個函式已經進行一次抽象了
// 等價于呼叫
unsigned int alarm(unsigned int seconds);
int pause(void);
sleep函式正常休眠,回傳0,如果因為信號中斷休眠,回傳剩余休眠的時間,
高精度
#include <time.h>
int nanosleep(const struct timespec *requested_time, struct timespec *remaining)
struct timespec
{
long tv_sec; /* Seconds. */
long tv_nsec; /* Nanoseconds. */
};
該函式的實作不依賴與信號,so???
- requested_time
- 指明休眠時間,支持納秒級別
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/285523.html
標籤:C
上一篇:Linux C 檔案IO
下一篇:C 語言通用模板佇列(宏函式)
