您的位置:首页 > 财经 > 产业 > 今日新冠疫情最新消息_深圳网站制作联系兴田德润_微信公众号平台官网_自动的网站设计制作

今日新冠疫情最新消息_深圳网站制作联系兴田德润_微信公众号平台官网_自动的网站设计制作

2025/1/7 4:04:29 来源:https://blog.csdn.net/qq_37409526/article/details/144827759  浏览:    关键词:今日新冠疫情最新消息_深圳网站制作联系兴田德润_微信公众号平台官网_自动的网站设计制作
今日新冠疫情最新消息_深圳网站制作联系兴田德润_微信公众号平台官网_自动的网站设计制作

本 Lab 需要实现一个简易的 shell,主要考察对进程和信号的理解,以及对与其相关的 POSIX API 的使用,对应知识点为书中的第 8 章内容。

思路

实验要求

实现一个简单的 shell,要求支持如下特性:

  1. 输入 ctrl-c 触发 SIGINT 信号,输入 ctrl-z 触发 SIGTSTP 信号,发送给给前台运行的任务和依赖于这些任务的子任务(子进程)。
  2. 如果命令行以 & 结尾,那么本次作业将被置于后台运行,否则置于前台运行。
  3. 每个作业可以通过 PID(process id)或 JID(job id)来指定,其中 JID 需要加上前缀 %
  4. 支持下列内建命令:
    1. quit:终止 shell 的运行。
    2. jobs:列出所有的后台作业。
    3. bg <job>:重启 <job>(PID 或者 JID),通过发出 SIGCONT 信号,然后将其运行在后台。
    4. fg <job>:重启 <job>(PID 或者 JID),通过发出 SIGCONT 信号,然后将其运行在前台。
  5. 回收所有的僵尸进程。

命令行解释执行

eval 函数的作用是解析并执行 shell 输入的命令行。对于内建命令(builtin command)而言,应该立即在 shell 中进行处理;而对于非内建命令而言,应该使用 fork + execve 的组合,创建一个指定的进程来进行处理。

tsh 不需要支持管道功能,因此一个作业只对应一个进程。

fork 的功能是创建一个子进程,该子进程的虚拟地址空间完全拷贝自其父进程,且程序计数器的位置同样位于该 fork 函数处。区别在于,父进程的返回值为子进程的 pid,子进程的 pid 为 0,因此可以根据这一特性来对父进程和子进程进行分别处理:

  • 子进程需要先使用 setpgid(0, 0) 将进程组 id 设置为自己的 pid,将自己与 tsh “脱离”,确保此时前台进程组只有 tsh 本身,防止被 tsh 接收到的信号所影响。然后再使用 execve,用指定的可执行程序替换拷贝自父进程的虚拟地址空间,并开始从头执行。
  • 父进程则需要根据本次输入的命令行,创建其对应的作业数据结构 struct job_t,并加入作业队列 jobs 中。同时,由于前台作业(命令行末尾不带 & 符号)需要一直占用终端,因此当本次作业为前台作业时,需要调用 waitfg 进行等待,直到作业的类型不再是前台运行(FG)。
if ((pid = fork()) == 0) {setpgid(0, 0);  // 将进程组设置为自己的 pidif (execve(argv[0], argv, environ) < 0) {printf("execve %s error\n", argv[0]);exit(1);}
} else {if (bg) {addjob(jobs, pid, BG, cmdline);printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);} else {addjob(jobs, pid, FG, cmdline);waitfg(pid);}
}

根据书中 8.5.6 节的描述,可能会出现一种情况:父进程 fork 子进程后,子进程一直被调度执行,直到运行结束称为一个僵尸进程(zombie)并向父进程发送 SIGCHLD 信号。等到父进程被调度时,他先响应 SIGCHLD 信号并将成为僵尸进程的子进程收割(reap),同时尝试将作业从作业队列中移除,此时事实上不会做任何事情,因为作业还没有加入到作业队列中。再接下来父进程继续执行 fork 后的代码,才将作业加入到作业队列中,这明显不对!

针对这类恼人的同步问题,可以采用 sigprocmask (如下所示是 sigprocmask 的使用说明)在适当的位置设置信号屏蔽字来解决。对于本问题,可以在父进程中先屏蔽对 SIGCHLD 信号的处理,等到使用 addjob 将作业加入作业队列后才开始响应 SIGCHLD 信号,这样即便是子进程先执行完毕并向父进程发送 SIGCHLD 信号,父进程也能确保在 addjob 执行完后才处理这一信号。

  • generated by GPT4o

sigprocmask 介绍

sigprocmask 是一个用于操作信号屏蔽字(signal mask)的系统调用。它允许程序暂时屏蔽或取消屏蔽某些信号的接收,进而控制信号的处理行为。通过它,程序可以避免在特定代码段中处理中断信号,从而进行更细粒度的控制。

sigprocmask 的原型:
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how
    • SIG_BLOCK:将 set 中的信号加入到当前信号屏蔽字中(即阻塞这些信号)。
    • SIG_UNBLOCK:从当前信号屏蔽字中移除 set 中的信号(即解除屏蔽这些信号)。
    • SIG_SETMASK:将当前信号屏蔽字设置为 set 中的信号集(即完全替换当前的信号屏蔽字)。
  • set:指向包含信号集的 sigset_t 类型变量,该变量指定要阻塞或解除阻塞的信号。
  • oldset:指向 sigset_t 类型的变量,用来保存当前的信号屏蔽字,以便稍后恢复。
信号集 sigset_t 的操作:
  • 初始化信号集:
    • sigemptyset(sigset_t *set):清空信号集。
    • sigfillset(sigset_t *set):将所有信号加入信号集。
    • sigaddset(sigset_t *set, int signum):将特定信号加入信号集。
    • sigdelset(sigset_t *set, int signum):从信号集中移除特定信号。

此外,为了防止其他可能的并发错误出现,可以在对全局变量的修改前先屏蔽所有信号。

sigset_t mask_all, mask_one, prev_one;sigfillset(&mask_all);
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
if ((pid = fork()) == 0) {sigprocmask(SIG_SETMASK, &prev_one, NULL);setpgid(0, 0);  // 将进程组设置为自己的 pidif (execve(argv[0], argv, environ) < 0) {printf("execve %s error\n", argv[0]);exit(1);}
} else {sigprocmask(SIG_BLOCK, &mask_all, NULL);if (bg) {addjob(jobs, pid, BG, cmdline);sigprocmask(SIG_SETMASK, &prev_one, NULL);printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);} else {addjob(jobs, pid, FG, cmdline);sigprocmask(SIG_SETMASK, &prev_one, NULL);waitfg(pid);}
}

bg/fg: 已停止作业恢复执行

在内建命令中,quit 直接让程序正常退出即可,jobs 只需调用已实现的函数 listjobs,真正需要实现的是 bgfg,它们的作用是将处于停止状态的作业恢复到后台(前台)执行。

实现方式无非分两步:

  1. 参数解析,提取出作业 ID(分为 PID 和 JID)。
  2. 根据 bgfg 的不同,分别进行处理,将作业运行在前台或是后台。

参数解析其实也就是字符串处理,这部分与本章的异常控制流关系不大,在此不过多赘述。

根据参数得到对应的作业 job 后,首先需要它的运行状态更改为指定的状态(BGFG),然后向 job 对应的进程组(kill 的 pid 参数为 -job->pid)发送 SIGCONT 信号,将内核在响应此信号后会重新调度该进程组内的所有进程执行。

sigprocmask(SIG_BLOCK, &mask_all, &prev_one);
job->state = state;
sigprocmask(SIG_SETMASK, &prev_one, NULL);
kill(-job->pid, SIGCONT);

最后,对于前台运行而言,父进程需要调用 waitfg 进行等待;对于后台运行而言,打印作业信息(具体格式见 tshref.out 的内容)。

if (state == FG) {waitfg(job->pid);
} else {printf("[%d] (%d)", job->jid, job->pid);for (int i = 0; argv[i] != NULL; ++i) {printf(" %s", argv[i]);}printf("\n");
}

信号处理函数

这部分看似简单,实则很容易出错,值得仔细说说。

最开始,我在 sigint_handlersigtstp_handler 中进行了对作业队列的操作,在 sigint_handler 中使用 deletejob 删除作业,在 sigtstp_handler 中将作业状态修改为 ST

这在前面的测试用例中工作一切正常,但在最后的 trace16 中,程序却阻塞在了 mystop.cif (kill(-pid, SIGTSTP) < 0) 位置,而如果按下键盘的 ctrl-c 或 ctrl-z 却能够响应信号,程序也正常终止或暂停。

通过查阅网上资料,在 实验四:Shell-Lab(下) - 知乎 找到了问题的原因。

程序 mystop 使用 kill 发送信号时,信号是直接发送到内核进行处理的,而不会经过 shell,shell 也就不会调用 sigtstp_handler 将作业状态更改为 ST,shell 因此认为 mystop 程序一直处于前台运行状态,也就保持阻塞。

既然 sigint_handlersigtstp_handler 在这种情况下无法被调用,那么就需要在其他信号处理函数中进行处理,答案便是 sigchld_handler。事实上,子进程并非只有在终止时才向父进程发送 SIGCHLD 信号,而是在状态发生改变后就会。我们可以利用这一点,将所有对作业队列的操作都放到 sigchld_handler 中,而 sigint_handlersigtstp_handler 只作一个转发作用。

void sigchld_handler(int sig) 
{...while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {if (WIFEXITED(status)) {             // 子进程正常退出...} else if (WIFSTOPPED(status)) {     // 子进程暂停...} else if (WIFSIGNALED(status)) {    // 子进程终止...}}
}

代码

eval

void eval(char *cmdline) 
{int bg;pid_t pid;char *argv[MAXARGS];sigset_t mask_all, mask_one, prev_one;  // 用于父-子进程同步sigfillset(&mask_all);sigemptyset(&mask_one);sigaddset(&mask_one, SIGCHLD);bg = parseline(cmdline, argv);if (builtin_cmd(argv)) return;if (access(argv[0], X_OK) != 0) {printf("%s: Command not found\n", argv[0]);return;}sigprocmask(SIG_BLOCK, &mask_one, &prev_one);if ((pid = fork()) == 0) {sigprocmask(SIG_SETMASK, &prev_one, NULL);setpgid(0, 0);  // 将进程组设置为自己的 pidif (execve(argv[0], argv, environ) < 0) {printf("execve %s error\n", argv[0]);exit(1);}} else {sigprocmask(SIG_BLOCK, &mask_all, NULL);if (bg) {addjob(jobs, pid, BG, cmdline);sigprocmask(SIG_SETMASK, &prev_one, NULL);printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);} else {addjob(jobs, pid, FG, cmdline);sigprocmask(SIG_SETMASK, &prev_one, NULL);waitfg(pid);}}
}

built_cmd

int builtin_cmd(char **argv) 
{if (!argv[0]) {return 1;} else if (strcmp(argv[0], "quit") == 0) {exit(0);} else if (strcmp(argv[0], "jobs") == 0) {listjobs(jobs);} else if (strcmp(argv[0], "bg") == 0) {do_bgfg(argv, BG);} else if (strcmp(argv[0], "fg") == 0) {do_bgfg(argv, FG);} else {return 0;}return 1;
}

do_bgfg

// 根据字符串 arg(格式为 %jid 或 pid)获取对应的 job
// 成功返回 job 指针,失败返回 NULL
struct job_t *getjob_arg(const char *arg) {int isjid, id;const char *parg;struct job_t *job;isjid = 0;if (arg[0] == '%') {isjid = 1;++arg;}parg = arg;while (*parg != '\0') {if (!isdigit(*parg)) {printf("bg/fg: argument must be a PID or %%jobid\n");return NULL;}++parg;}id = atoi(arg);if (isjid) {job = getjobjid(jobs, id);if (job == NULL) {printf("(%s): No such process\n", arg);}} else {job = getjobpid(jobs, id);if (job == NULL) {printf("%s: No such job\n", arg);}}return job;
}// 与题目提供的接口有些不同(加了个 state 参数),感觉这样更好处理一些...
void do_bgfg(char **argv, int state) 
{sigset_t mask_all, prev_one;struct job_t *job;sigfillset(&mask_all);if (argv[1] == NULL) {printf("fg command requires PID or %%jobid argument\n");return;}job = getjob_arg(argv[1]);if (job == NULL) {return;}sigprocmask(SIG_BLOCK, &mask_all, &prev_one);job->state = state;sigprocmask(SIG_SETMASK, &prev_one, NULL);kill(-job->pid, SIGCONT);if (state == FG) {waitfg(job->pid);} else {printf("[%d] (%d)", job->jid, job->pid);for (int i = 0; argv[i] != NULL; ++i) {printf(" %s", argv[i]);}printf("\n");}
}

waitfg

void waitfg(pid_t pid)
{while (1) {struct job_t *job = getjobpid(jobs, pid);if (job == NULL || job->state != FG) break;}
}

sigchld_handler

void sigchld_handler(int sig) 
{pid_t pid;int status;sigset_t mask_all, prev_one;sigfillset(&mask_all);while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {sigprocmask(SIG_BLOCK, &mask_all, &prev_one);if (WIFEXITED(status)) {deletejob(jobs, pid);} else if (WIFSTOPPED(status)) {printf("Job [%d] (%d) stopped by signal 20\n", pid2jid(pid), pid);getjobpid(jobs, pid) -> state = ST;} else if (WIFSIGNALED(status)) {printf("Job [%d] (%d) terminated by signal 2\n", pid2jid(pid), pid);deletejob(jobs, pid);}sigprocmask(SIG_SETMASK, &prev_one, NULL);}
}

sigint_handler

void sigint_handler(int sig) 
{pid_t pid = fgpid(jobs);kill(-pid, sig);
}

sigtstp_handler

void sigtstp_handler(int sig) 
{pid_t pid = fgpid(jobs);kill(-pid, sig);
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com