参考大佬
前情提要
eval
: Main routine that parses and interprets the command line. 70 linesbuiltin cmd
: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs. 25 linesdo bgfg
: Implements the bg and fg built-in commands. 50 lineswaitfg
: Waits for a foreground job to complete. 20 linessigchld handler
: Catches SIGCHILD signals. 80 linessigint handler
: Catches SIGINT (ctrl-c) signals. 15 linessigtstp handler
: Catches SIGTSTP (ctrl-z) signals. 15 linesshell通常用来指命令行,我们可以通过这个命令行去启动各种程序
那么,这个命令行是如何启动这些程序的呢?
tsh>
eval函数的功能其实类似于一个启动器
第一步,获取各种输入的信息,这个就比较简单了,因为已经提供了写好的parseline
函数,我们只需要调用即可。
// 首先根据cmdline,解析这一行命令。由于我们在解析的函数中修改了传入的cmdline,所以用了一个备份的buffer。
// 同时解析函数的返回值其实就代表了当前这个被命令行启动的进程是前天还是后台运行的
// 但是其实好像不备份也行,留坑!
char buffer[MAXLINE];
strcpy(buffer, cmdline);
char *argv[MAXARGS];
int is_bg = parseline(buffer, argv);
// 题目已经定义为bg和fg,所以按照题目意思来定义状态
int state = is_bg ? BG : FG;
// 如果解析出来是个空的命令行,则直接return
if (argv[0] == NULL) {
return;
}
第二步,就比较复杂了。
首先想一想,父进程需要做什么?
父进程
SIGCHLD
信号
execve
函数启动对应的程序子进程这里需要注意一点,那就是用户输入的程序可能是不存在的,因此,对于execve函数的结果需要特殊处理一下
整个eval函数的实现如下,其中涉及了一些信号相关的操作比较陌生
void eval(char *cmdline) {
// 首先根据cmdline,解析这一行命令。由于我们在解析的函数中修改了传入的cmdline,所以用了一个备份的buffer。
// 同时解析函数的返回值其实就代表了当前这个被命令行启动的进程是前天还是后台运行的
// 但是其实好像不备份也行,留坑!
char buffer[MAXLINE];
strcpy(buffer, cmdline);
char *argv[MAXARGS];
int is_bg = parseline(buffer, argv);
// 题目已经定义为bg和fg,所以按照题目意思来定义状态
int state = is_bg ? BG : FG;
// 如果解析出来是个空的命令行,则直接return
if (argv[0] == NULL) {
return;
}
// 下面是核心操作部分
// 创建信号集并初始化,分别是不理会所有信号,不理会一个信号(SIGCHLD),和备份之前的信号
sigset_t all_mask, one_mask, pre_mask;
sigfillset(&all_mask);
sigemptyset(&one_mask);
sigaddset(&one_mask, SIGCHLD);
// 如果即将创建的进程不是系统程序,那下面这个函数会返回0
// 如果是系统程序,那这个函数内部就直接执行了, 不需要我们管了
// 因此,我们只需要处理非系统程序的情况
pid_t pid;
if (!builtin_cmd(argv)) {
// 在调用fork之前就要阻塞SIGCHLD信号
sigprocmask(SIG_BLOCK, &one_mask, &pre_mask);
if ((pid = fork()) == 0) {
// 子进程
// 子进程首先解除从父进程那继承来的阻塞信号
sigprocmask(SIG_SETMASK, &pre_mask, NULL);
// 修改自己的组id为自己的id
setpgid(0, 0);
// 创建真正的进程,其中environ是在libc文件中定义的
Execve(argv[0], argv, environ);
// 真正的进程执行完之后,子进程也就完成使命了,使用exit终止进程,更加强劲!
exit(0);
}
// 父进程继续操作
// 首先获取全局锁,因为要修改全局变量了
sigprocmask(SIG_BLOCK, &all_mask, NULL);
addjob(jobs, pid, state, cmdline);
sigprocmask(SIG_SETMASK, &one_mask, NULL);
// 如果是前台进程,则父进程要阻塞到这个前台进程结束
// 如果是后台进程,则父进程打印这个后台进程的信息
if (state == FG) {
waitfg(pid);
} else {
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
// 父进程ok了,可以去接受SIGCHLD信号
sigprocmask(SIG_SETMASK, &pre_mask, NULL);
}
}
这个函数就比较简单了,用来判断用户输入的是否是内置的程序
tsh中要求的内置程序只有quit
,bg
,fg
,jobs
这个实现没什么好说的,可能有点前置的知识那就是,argv这个变量,可以看做一个一维数组,其中
每个值都是一个字符串
int builtin_cmd(char **argv) {
// 总共要处理 quit bg fg jobs,并&
if (!strcmp(argv[0], "quit")) {
// quit指令,直接终止
exit(0);
}
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
// 执行bg或者fg
do_bgfg(argv);
return 1;
}
if (!strcmp(argv[0], "jobs")) {
// 列出jobs
listjobs(jobs);
return 1;
}
return 0;
}
void do_bgfg(char **argv)
这个函数是要我们将某个job唤醒,或者由后台操作变成前台操作
argv[0]
中SIGCONT
的信号,不管它之前咋样,现在都醒过来void do_bgfg(char **argv) {
// 后面要用到
struct job_t *job = NULL;
// 这个函数需要处理多种输入,包括一些非法输入
// 首先是确认到底是bg操作还是fg操作
int state = (strcmp(argv[0], "bg") == 0) ? BG : FG;
// 然后判断是否给出了具体的pid或者jid,如果没有给出正确的参数,那要给出提示
// 首先看看是否有参数
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
// 有参数,那就先看看是不是jid
if (argv[1][0] == '%') {
// 使用sscanf尝试获取jid
int jid;
if (sscanf(&argv[1][1], "%d", &jid) > 0) {
job = getjobjid(jobs, jid);
// 如果getjobjid返回null,则说明没有这个job
if (job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
}
// 到了这里,那肯定有参数,并且不是jid,但是也有可能是瞎输入的先排除一下
} else if (!isdigit(argv[1][0])) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
} else {
// 肯定输入的是pid了
pid_t pid;
if (sscanf(argv[1], "%d", &pid) > 0) {
job = getjobpid(jobs, pid);
if (job == NULL) {
printf("(%d): No such process\n", pid);
return;
}
}
}
// 如果能够走到这里,那么说明已经正确取到对应的job了
// 唤醒这个job,修改状态
// 这里没有使用进程组,留坑!
kill(-job->pid, SIGCONT);
job->state = state;
// 根据bg或者fg进行特定的操作
if (state == BG) {
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else {
waitfg(job->pid);
}
}
void waitfg(pid_t pid)
这个函数需要做到,当pid还是前台程序的时候,调用waitfg的程序一直休眠
首先,lab已经给我们提供了一个函数fgpid
去检查是否还有程序是前台程序
所以,我们最好还是使用信号的机制,书上介绍了一个很牛逼的函数sigsuspend
具体的优点书上已经详细介绍了,这里不赘述
这里就有个问题,正常来说,调用这个函数之前,进程应该是阻塞了SIGCHLD
信号才对的
而我的实现里,有两处调用了这个函数,其中do_bgfg是没有阻塞上述的那个信号,但是也没有出现死锁,应该是测试数据太水了导致的
void waitfg(pid_t pid) {
// 注意,进入这个函数的时候,父进程已经阻塞了子进程可能传来的SIGCHLD信号
// 而我们使用的sigsuspend函数会让父进程进入一个完全没有阻塞信号的状态
// 因此,只要有一个子进程挂了,父进程就会跳出阻塞去检查一下是否还有前台进程
// 感觉有点问题,这里实现的好像太严格了,不只考虑了pid,还考虑所有前台进程,留坑
sigset_t none_mask;
sigemptyset(&none_mask);
while (fgpid(jobs) != pid) {
sigsuspend(&none_mask);
}
}
这个函数和SIGCHLD
信号绑定了,只要有子进程发出这个信号,父进程接收到之后就会使用这个函数去响应
注意点
3. WNOHANG | WUNTRACED
1. 第一个参数保证了,就算没有需要处理的子进程,当前进程也不会阻塞住不动
2. 第二个参数保证了,会去响应被暂停的子进程
4. 备份errno
5. 在修改jobs之前需要阻塞所有信号,作用和之前说的一样,防止并发带来的问题
由于我们不知道到底有多少个死亡信号或者暂停信号到达,所以必须要用while,而不是if
void sigchld_handler(int sig) {
int olderr = errno;
pid_t pid;
int state;
sigset_t all_mask, pre_mask;
sigfillset(&all_mask);
// 不断等待子进程,直到收到一个子进程停止的消息
while ((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0) {
// 因为接下来需要操作jobs这个全局变量,先屏蔽所有的信号
sigprocmask(SIG_SETMASK, &all_mask, &pre_mask);
// 如果是正常终止,则只需要删除对应的job
if (WIFEXITED(state)) {
deletejob(jobs, pid);
// 如果是被信号杀死,则还需要输出被哪个信号杀死
} else if (WIFSIGNALED(state)) {
deletejob(jobs, pid);
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid,
WTERMSIG(state));
// 如果是被信号暂停了,还需要修改对应job的状态
} else if (WIFSTOPPED(state)) {
struct job_t *job = getjobpid(jobs, pid);
job->state = ST;
printf("Job [%d] (%d) stoped by signal %d\n", pid2jid(pid), pid,
WSTOPSIG(state));
}
sigprocmask(SIG_SETMASK, &pre_mask, NULL);
}
errno = olderr;
}
这个函数对应于ctrl c操作,强制杀死前台的进程
实现起来很简单:找到当前的前台进程,然后通过kill函数杀死它
注意保存和恢复error
void sigint_handler(int sig) {
// 前台进程只可能有一个,只要还存在前台进程,那就把这个前台进程给杀掉
// 感觉这里不加信号量也没问题
int olderr = errno;
pid_t pid;
if ((pid = fgpid(jobs)) != 0) {
kill(pid, sig);
}
errno = olderr;
}
这个函数对应于ctrl z函数,暂停前台进程
int olderr = errno;
pid_t pid;
if ((pid = fgpid(jobs)) != 0) {
kill(pid, sig);
}
errno = olderr;