- 信號處理器函數設置全局標誌變量並退出,主程序對此標記進行週期檢查,一旦置位隨即採起相應動做
- 信號處理器函數執行某種類型的清理,以後終止進程或者使用非本地跳轉將棧解開並將控制返回主程序定位置
- 可重入:
- 同一進程多線程能夠同時安全的調用一個函數(信號處理函數可在任意時間點中斷程序的執行,從而在一個進程中造成2條獨立但不是併發的執行線程)
- 異步信號安全:
- 若是一個函數是可重入的或者信號處理函數沒法將其中斷
- 僅當信號處理函數中斷了不安全函數的執行,且處理函數自身也調用了這個函數時,該函數纔是不安全的
實現的兩種策略linux
- 確保信號處理器函數代碼自己是可重入的,且只調用異步信號安全的函數
- 當主程序執行不安全函數或者是去處理信號處理器函數可能更新的全局數據結構時,阻塞信號的傳遞
全局變量和sig_atomic_tredis
volatile
避免將全局變量優化的寄存器sig_atomic_t
保證讀寫原子性- 主程序和信號處理器函數共享的全局變量聲明以下
volatile sig_atomic flag
信號處理器函數終止的方法shell
- 調用_exit(),不是exit()(會清除I/O緩衝不安全)
- 調用kill()來殺掉進程
- 信號處理器函數執行非本地跳轉:
- 略
- 調用abort()終止進程併產生核心轉儲
- 略
系統調用的中斷和重啓編程
信號處理器函數中斷了阻塞的系統調用時,系統調用會產生EINTR報錯api
- 內含進程終止時內存鏡像的一個文件(默認進程工做目錄/core)
- 文件命名/proc/sys/kernel/core_pattern
SIGKILL
和SIGSTOP
的默認行爲沒法更改,調用signal()
和sigaction()
來改變時總會返回錯誤;同時也不能阻塞SIGCONT
若是一個進程處於中止狀態,SIGCONT
總會使其回覆;進程收到SIGCONT
會將等待狀態的中止信號丟棄,反之收到中止信號,會將等待狀態的SIGCONT
丟棄
內核常常須要領進程休眠,休眠有兩種安全
- TASK_INTERRUPTIBLE:等待某一事件(終端輸入等等),傳遞進來的信號會喚醒進程 ps STAT :S
- TASK_UNINTERRUPTIBLE:等待特定事件(磁盤I/O完成等的),在擺脫狀態前,系統不會把信號傳遞給進程 ps STAT:D
- 通常這種狀態轉瞬即逝,可是若是由於硬件故障等問題 由於(
SIGKILL
,SIGSTOP
)不會終止掛起進程,只能經過重啓- TASK_KILLABLE:相似於前者,可是收到殺死進程信號會喚起
- 同步信號(進程本身產生的信號,本身調用kill(), raise()等等)會當即傳遞
- 異步信號在進程發生內核態到用戶態的下一次切換時
- 進程在前度超時後,再度得到調用
- 系統調用完成
若是經過
sigprocmask()
解除了多個等待信號的阻塞。這些信號會馬上傳給進程bash
- Linux下一般是根據信號編號升序傳遞
- 同時若是調度器函數引發了內核態和用戶態的切換,會中斷函數轉而調用下一個信號處理器函數
- 信號範圍擴大
- 隊列化管理,若是將某一實時信號的多個實例發給一進程,信號會屢次傳遞
- 可爲信號指定伴隨數據
- 傳遞順序獲得保障,等待恢復後(信號編號小加時間早先傳遞)
#define _POSX_C_SOURCE 199309
#include<signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// 權限和kill()一致,可是pid不能爲負
union sigval{
int sival_int;
void *sival_ptr;
}
複製代碼
#include<signal.h>
int sigsuspend(const sigset_t *mask);
//sigsuspend(&mask) 等同於
sigprocmask(SIG_SETMASK, &mask, &prevMask);
pause();
sigprocmask(SIG_SETMASK, &prevMask, NULL);
複製代碼
調用,捕獲,文件描述符獲取信號略數據結構
限制比較多多線程
- 信號異步的本質,可重入性,竟態條件,全局變量的設置
- 標準信號沒法排隊,實時信號排隊數量限制
- 攜帶信息有限
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
#include <unistd.h>
void sig_handler(int signum) {
printf("in handler\n");
sleep(1);
printf("handler return\n");
}
int main(int argc, char **argv) {
char buf[100];
int ret;
struct sigaction action, old_action;
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
/* 版本1:不設置SA_RESTART屬性 * 版本2:設置SA_RESTART屬性 */
//action.sa_flags |= SA_RESTART;
sigaction(SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT, &action, NULL);
}
bzero(buf, 100);
ret = read(0, buf, 100);
if (ret == -1) {
perror("read");
}
printf("read %d bytes:\n", ret);
printf("%s\n", buf);
return 0;
}
// ctr+c 以後一個會ret=-1 一個會從新執行
複製代碼
#include<sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
// 0 success, -1 on error
int getitimer(int which, struct itimerval *curr_value);
// curr_value值和old_value值一致
struct itimerval{
struct timeval it_interval;
struct timeval it_value;
};
struct timeval{
time_t tv_sec;
suseconds_t tv_usec;
}
複製代碼
which取值併發
- ITIMER_REAL:真實時間倒計時的定時器,到期產生SIGALARM信號發送給進程
- ITIMER_VIRTUAL:用戶模式下CPU時間倒計時計時器,到期產生SIGVTALRM
- ITIMER_PROF:進程時間(用戶態和內核態CPU時間總和)的倒計時器,到期產生SIGPROF
new_value取值
- it_value 指定了延遲時間
- it_interval若是兩個字段爲0,則爲一次性定時器,不然,在每次定時器到期後,會重置定時器指定間隔後再到期
- 若是調用setitimer時new_value 的 it_value的字段都爲0,則屏蔽已有定時器
SUSv4廢棄了
getitimer()
和setitimer()
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
// 一次性定時器,返回剩餘時間,alarm(0)屏蔽全部定時器,根據操做系統實現決定是否是和setitimer共享屏蔽
複製代碼
- 可經過定時器函數結合
sigsuspend
實現sleep
include<unistd.h>
unsigned int sleep(unsigned int seconds);
//正常結束返回0,若是被終止返回剩餘秒數
複製代碼
略
- 系統調用
fork()
:子進程獲取父進程的棧,數據段,堆和執行文本段的拷貝,同一程序文本段- 庫函數
exit(status)
(時系統調用_exit(status)
的外層),將進程所佔用的全部資源歸還內核;父進程經過wait()
獲取該狀態- 系統調用
wait(&status)
- 若是子進程未經過
exit()
終止,掛起父進程直至子進程終止- 子進程狀態經過
status
參數返回- 父子進程通常只有一個會
exit()
退出,另外一個使用_exit()
退出- 系統調用
execve(pathname, argv, envp)
加載一個新程序到當前內存,丟棄現存的程序文本段,並從新建立棧,數據段以及堆
fork()
完成
fork()
的調用後兩個進程均會從fork()
處返回,父進程返回子進程的pid,子進程返回0,沒法建立時返回-1(多是進程數超過了real_user_id的進程數上限或者系統級上限)
pid_t childPid;
switch (childPid = fork()){
case -1:
/*...*/
case 0:
/* child perform */
default:
/* parent perform */
}
複製代碼
fork()
文件描述符的建立相似dup()
,指向相同的打開文件句柄,即文件偏移量等均是共享的,父子進程不會覆蓋彼此的輸出,但會亂序;shell中不加&
父進程會等待子進程結束
fork()
內存語義
- 原意是對程序段,數據段,堆段和棧段建立拷貝
- 會形成浪費,好比
fork()
以後的exec()
從新初始化....- 優化的措施
- 內核將每個進程的代碼段標記成RO,fork()爲子進程的構建代碼段時,其所構建的一系列進程級頁表均指向父進程相同那個的物理內存頁幀
- 數據段,堆段,棧段中的各頁,內核採用寫時複製(copy-on-write)以前(redis BUG有遇到過):將這些段的頁表指向父進程物理地址相同的物理內存頁,並將這些頁標記成只讀,以後爲要修改的頁創建copy
vfork()
- 儘可能避免使用
- 適用於爲子進程馬上調用
exec()
而設計- 無需爲子進程複製虛擬內存頁活頁表,共享父進程內存,直到成功調用exec()或_exit(),(文件描述符表每一個進程是獨立的)
- 在子進程調用exec()和_exit()以前暫停父進程
- 能保證調用
vfork()
以後子進程先於父進程得到CPU調度
fork()
以後的競態條件
/proc/sys/kernel/sched_child_runs_first
爲0 則fork()
以後父進程先調度
- 經過信號終止進程,可能產生核心轉儲。
- 經過調用
_exit()
正常終止,main()
函數return n
等同於exit(n)
- 若是在推出的處理過程當中所執行的任何步驟須要訪問
main()
本地變量,那麼從main()
返回會致使未定義的行爲(ex:setbuff()
調用本地變量)
#include<unistd.h>
void _exit(int status);
//調用永遠成功
複製代碼
exit()
#include<stdlib.h>
void exit(in status);
// 不是異步信號安全函數
複製代碼
- 調用退出處理程序(經過
atexit()
和on_exit()
註冊的函數),執行順序與註冊順序相反- 刷新
stdio
流緩衝區- 使用
status
執行_exit()
- 關閉全部的打開文件描述符,目錄流,信息目錄描述符?以及轉換描述符?
- 以後章節涉及的細節待補充。
#include<stdlib.h>
int atexit(void (*func) (void));
// 0 on success
複製代碼
#define _BSD_SOURCE
#include<stdlib.h>
int on_exit(void (*func)(int, void *), void *args);
// 和 atexit是在同一列表註冊,同樣的執行和註冊順序相反
複製代碼
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(){
printf("Hello\n");
write("STDOUT_FILENO", "WORLD\n",6);
if(fork() == -1){
exit(-1);
}
exit(EXIT_SUCCESS);
}
複製代碼
$ gcc fork_io.c -o fork_io
$ ./fork_io
Hello
WORLD
$ ./fork_io > 1.txt | cat 1.txt
WORLD
Hello
Hello
//終端爲行緩衝,重定向到文件爲塊緩衝
複製代碼
避免方法
fork()
以前fflush()
刷新緩衝區,或調整stdio的緩衝選擇- 確認的狀況下子進程調用_exit()(應該不是很合適)