shlab這節是要求寫個支持任務(job)功能的簡易shell,主要考察了linux信號機制的相關內容。難度上如果熟讀了《CSAPP》的“異常控制流”一章,應該是可以不算困難的寫出來。但如果讀書不仔細,或者實踐的時候忘記了部分細節,那就可能完全不知道怎么下手,或者得改bug改到吐了。我自己寫了大概八個小時,其中僅一半的時間都在處理收到SIGTSTP
后莫名卡死的問題,最后才發現是課本沒看仔細,子進程停止后也會向父進程發送SIGCHLD
。
在實驗中我們需要實現job、fg、bg、kill四個內建命令和對執行本地程序的支持,並且還要處理好SIGCHLD
、SIGINT
、SIGTSTP
這幾個信號。關鍵要點都在課本的534頁有說過了:
-
處理程序盡可能簡單
-
處理程序中只用異步信號安全的函數
-
保存恢復errno
-
訪問共享全局變量時阻塞所有信號
-
volatile聲明全局變量
-
sig_atiomic_t聲明標志
驗收標准這一塊因為是在實際操作系統上跑的,不能保證進程號相同,但要保證處理進程號意外所有指令的順序和信息都要與參考程序的輸出完全相同。這點可以用linux上的各種diff工具進行結果比較。
eval
eval函數在課本P525頁有一個缺陷版,我們要做的就是以此為藍本加上點信號處理。
void eval(char* cmdline)
{
char* argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) {
return;
}
if (!builtin_cmd(argv)) {
sigset_t mask_chld, prev_mask, mask_all;
sigemptyset(&mask_chld);
sigaddset(&mask_chld, SIGCHLD);
sigfillset(&mask_all);
/*因為子進程可能在addjob前就結束並調用deleltejob,所以我們要先阻塞掉SIGCHLD,
保證addjob操作成功*/
sigprocmask(SIG_BLOCK, &mask_chld, &prev_mask);
if ((pid = fork()) == 0) {
//子進程默認繼承父進程的mask,所以這里要恢復一下
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
setpgid(0, 0); //令進程組號等於進程號
if (execve(argv[0], argv, environ) <= 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
// addjob涉及到全局變量的操作,需要保證操作的原子性,故這里阻塞掉所有信號
sigprocmask(SIG_SETMASK, &mask_all, NULL);
addjob(jobs, pid, bg?BG:FG, cmdline);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
// 在線程終止前需要打印些相關信息,所以addjob完還要阻塞一會兒SIGCHLD
sigprocmask(SIG_BLOCK, &mask_chld, NULL);
if (!bg) {
waitfg(pid);
} else {
// 同上,操作全局變量時阻塞
sigprocmask(SIG_SETMASK, &mask_all, NULL);
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
// 操作結束后解除阻塞
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
}
return;
}
waitfg
waitfg負責等待前台進程結束。每次都調用fgpid有點低效了,我們直接用一個全局標志fg_child_flag
代表前台進程是否異常,默認為0,如果切換到停止或退出狀態就置1。
void waitfg(pid_t pid)
{
sigset_t mask_empty;
sigemptyset(&mask_empty);
fg_child_flag = 0;
while(!fg_child_flag){
// 參考課本545頁,掛起進程直到任意信號到達
sigsuspend(&mask_empty);
}
return;
}
sigchld_handler
要注意子進程終止或停止都可能觸發SIGCHLD
,所以我們得分類討論。
void sigchld_handler(int sig)
{
int olderrno=errno;
sigset_t mask_all, prev_mask;
pid_t pid;
int status;
sigfillset(&mask_all);
// 這里一定要設置成WUNTRACED,否則在子進程處於停止狀態時會卡死
if((pid=waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// 涉及到對全局變量jobs的訪問,阻塞所有信號
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
struct job_t *job = getjobpid(jobs, pid);
if(job->state == FG){ // 子進程為前台進程,打開標志
fg_child_flag=1;
}
if(WIFEXITED(status)){ // 正常退出,刪除任務即可
deletejob(jobs, pid);
}
else if(WIFSIGNALED(status)){ // 收到信號非正常退出,打印消息后刪除任務
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if(WIFSTOPPED(status)){ // 子進程處於停止狀態,切換對應的任務狀態
job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", job->jid ,pid, WSTOPSIG(status));
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
}
errno=olderrno;
return;
}
sigint_handler
因為進程在終止時會自動向父進程發送SIGCHLD
信號,所以部分邏輯放在了sigchld_handler,這里只要對子進程發出SIGINT
信號就行
void sigint_handler(int sig)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 訪問全局變量,阻塞所有信號
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
int pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
if(pid > 0){
kill(-pid, SIGINT); // 對子進程及其后代發送,故加負號
}
return;
}
sigtstp_handler
設計思路同上
void sigtstp_handler(int sig)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 訪問全局變量,阻塞所有信號
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
int pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
if(pid > 0){
kill(-pid, SIGTSTP); // 對子進程及其后代發送,故加負號
}
return;
}
builtin_cmd
仍舊參考課本525頁,挨個命令strcmp
就行
int builtin_cmd(char** argv)
{
if (!strcmp(argv[0], "quit")) {
exit(0);
}
if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "jobs")) {
//訪問全局變量,阻塞所有信號
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
listjobs(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
return 1;
}
if(!strcmp(argv[0], "kill")){
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "&")){
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg
這個也沒啥難度,對着參考輸出慢慢地添加判斷細節就行
void do_bgfg(char** argv)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 訪問全局變量jobs,阻塞所有信號
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
struct job_t *job;
int pid;
if(argv[1] == NULL){
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
else if(argv[1][0] == '%'){
int jid = atoi(argv[1] + 1);
job = getjobjid(jobs, jid);
if(job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
pid = job->pid;
}
else {
pid = atoi(argv[1]);
if(pid <= 0){
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
job = getjobpid(jobs, pid);
if(job == NULL){
printf("(%d): No such process\n", pid);
return;
}
}
if(!strcmp(argv[0], "bg")){
job->state = BG;
printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid, SIGCONT); // 對子進程及其后代發送,故加負號
return;
}
else if(!strcmp(argv[0], "fg")){
job->state = FG;
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid, SIGCONT); // 對子進程及其后代發送,故加負號
waitfg(pid); // 子進程切換到了前台,故要等待它執行完
return;
}
else if(!strcmp(argv[0], "kill")){
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid,SIGQUIT); // 對子進程及其后代發送,故加負號
return;
}
return;
}
到這所有的實現都捋完一遍了。做這個實驗的緣由是在看數據庫網課,講到緩存管理的時候老師說這一塊兒知識和你們學操作系統文件系統管理的知識一個樣,只不過我們為了效率得另寫一套。然后我發現這塊知識快忘光了,得補補操作系統,剛好CSAPP還剩下幾個實驗當初不屑做,干脆一塊搞了吧。