这个实验需要我们在tsh.c中补充相关代码实现一个shell程序(废话ing)
那么根据README文档中的顺序进行补齐

  1. 添加相关的宏定义和头文件
1
2
3
4
5
#define _POSIX_C_SOURCE 199309L
#define _XOPEN_SOURCE 500

#include <sys/select.h>
#include <sys/time.h>

eval

解析和解释命令行的主例程

1. 实现内容

  1. 若用户请求的是内置命令(quit、jobs、bg 或 fg)则立即执行。否则,创建一个子进程并在该子进程的上下文中运行该任务
  2. 若任务在前台运行,等待其终止后再返回
  3. 每个子进程必须具有唯一的进程组ID,这样在键盘上输入ctrl-c(ctrl-z)时,后台运行的子进程就不会从内核接收SIGINT(SIGTSTP)信号

2. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//166行
void eval(char *cmdline)
{
char* argv[MAXARGS]; // 存储解析后的命令参数
int state = UNDEF; // 进程状态(前台/后台)
sigset_t set;
pid_t pid;

if(parseline(cmdline, argv) == 1) {
state = BG; // 后台进程(以&结尾)
}else{
state = FG; // 前台进程
}
if(argv[0] == NULL) return;

if(!builtin_cmd(argv)){ // 如果不是内置命令,继续执行外部命令
// 初始化信号集并阻塞SIGINT、SIGTSTP、SIGCHLD信号
if(sigemptyset(&set) < 0){
unix_error("sigemptyset error");
}

if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0){
unix_error("sigaddset error");
}

if(sigprocmask(SIG_BLOCK, &set, NULL) < 0){
unix_error("sigprocmask error");
}
// 创建子进程执行命令
if((pid = fork()) < 0){
unix_error("fork error");
}else if(pid == 0){ // 子进程
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0){ // 解除信号阻塞
unix_error("sigprocmask error");
}

if(setpgid(0, 0) < 0){ // 设置新的进程组
unix_error("setpgid error");
}

if(execve(argv[0], argv, environ) < 0){ // 执行命令
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}

addjob(jobs, pid, state, cmdline); // 添加任务到任务列表
if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0){ // 解除信号阻塞
unix_error("sigprocmask error");
}
if(state == FG){
waitfg(pid); // 等待前台进程完成
}else{ // 后台进程:打印任务信息
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}
return;
}

builtin_cmd

识别和解释内置命令:quit、fg、bg和jobs

1. 实现内容

  1. 如果用户输入的是内置命令,则立即执行该命令

2. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int builtin_cmd(char **argv) 
{
if(!strcmp(argv[0], "quit")){ // 检查"quit"命令 - 退出shell
exit(0);
}else if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")){ // 检查后台/前台命令 - bg或fg
do_bgfg(argv); // 执行后台/前台任务切换
}else if(!strcmp(argv[0], "jobs")){ // 检查"jobs"命令 - 列出所有作业
listjobs(jobs);
}else{ // 不是内置命令,返回0让调用者处理外部命令
return 0; /* not a builtin command */
}
return 1; // 如果是内置命令且已处理,返回1
// return 0; /* not a builtin command */
}

do_bgfg

1. 实现内容

执行内置的bg和fg命令

2. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void do_bgfg(char **argv) 
{
if(!argv[1]){ // 检查是否提供了参数(PID或作业ID)
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

int jid = -1; // 作业ID
pid_t pid = -1; // 进程ID

// 解析参数:判断是作业ID(以%开头)还是进程ID
if(argv[1][0] == '%'){ // 处理作业ID格式:%jobid
jid = atoi(&argv[1][1]); // 跳过%字符,转换剩余部分为整数
if(jid <= 0) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
}else{ // 处理进程ID格式:pid
pid = atoi(argv[1]); // 直接转换参数为整数
if (pid <= 0) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
}

// 根据ID类型查找对应的作业
struct job_t *job = NULL;
if(jid > 0){ // 通过作业ID查找作业
job = getjobjid(jobs, jid);
if(!job){
printf("%%%d: No such job\n", jid);
return;
}
}else{ // 通过进程ID查找作业
job = getjobpid(jobs, pid);
if (!job) {
printf("(%d): No such process\n", pid);
return;
}
}

// 向整个进程组发送SIGCONT信号,恢复暂停的进程,使用-(job->pid)表示向整个进程组发送信号
kill(-(job->pid), SIGCONT);

// 根据命令类型进行不同处理
if(strcmp(argv[0], "fg") == 0){
job->state = FG; // 更新作业状态为前台
waitfg(job->pid); // 等待前台作业完成
}else{
job->state = BG; // 更新作业状态为后台
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline); // 打印作业信息,用户知道哪个后台作业在运行
}
}

waitfg

1. 实现内容

waitfg - 等待进程pid不再处于前台运行状态(该命令会阻塞当前进程,直到指定pid的进程退出前台运行状态)

2. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void waitfg(pid_t pid)
{
if(pid == 0)return; // 如果pid为0,直接返回(无效进程ID)

// 循环检查指定进程是否仍然是前台进程,fgpid(jobs)返回当前前台进程的PID,如果不是pid则说明进程已结束
while (fgpid(jobs) == pid){
sleep(0.1);
}

usleep(10000); // 休眠100毫秒,避免忙等待消耗CPU资源

// return;
}

sigchld_handler

1. 实现内容

每当一个子作业终止(成为僵尸进程),或因收到SIGSTOP或SIGTSTP信号而停止时,内核会向shell发送SIGCHLD信号。该处理器会回收所有可用的僵尸子进程,但不会等待任何其他当前正在运行的子进程终止

2. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void sigchld_handler(int sig) 
{
// 保存原始errno,保证信号处理函数的可重入性
int old_errno = errno;
pid_t pid;
int status; // 子进程状态信息

// 使用waitpid循环处理所有状态改变的子进程
// -1: 等待任何子进程
// WNOHANG: 非阻塞模式,立即返回
// WUNTRACED: 也报告停止的子进程
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
if(WIFEXITED(status)){ // 情况1: 子进程正常退出
deletejob(jobs, pid);
}else if(WIFSIGNALED(status)){ // 情况2: 子进程被信号终止
printf("Job [%d] (%d) terminated by signal %d\n",
pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}else if(WIFSTOPPED(status)){ // 情况3: 子进程被信号停止(如Ctrl+Z)

printf("Job [%d] (%d) stopped by signal %d\n",
pid2jid(pid), pid, WSTOPSIG(status));
// 获取对应的作业并更新状态为ST(停止)
struct job_t *job = getjobpid(jobs, pid);
if(job){
job->state = ST; // 设置为停止状态
}
}
}

errno = old_errno; // 恢复原始errno值

// return;
}

sigint_handler

1. 实现内容

当用户在键盘上输入ctrl-c时,内核会向shell发送SIGINT信号。捕获该信号并将其传递给前台作业。

2. 代码实现

1
2
3
4
5
6
7
8
9
void sigint_handler(int sig) 
{
pid_t fg_pid = fgpid(jobs); // 获取当前前台进程的PID
// 如果存在前台进程,向整个进程组发送SIGINT信号
if(fg_pid != 0){
kill(-fg_pid, SIGINT); // 使用负PID表示向整个进程组发送信号
}
// return;
}

sigtstp_handler

1. 实现内容

当用户在键盘上输入ctrl-z时,内核会向shell发送SIGTSTP信号。捕获该信号并通过向当前前台作业发送SIGTSTP信号来暂停该作业

2. 实现代码

1
2
3
4
5
6
7
8
9
void sigtstp_handler(int sig) 
{
pid_t fg_pid = fgpid(jobs);
// 如果存在前台进程,向整个进程组发送SIGTSTP信号
if(fg_pid != 0){
kill(-fg_pid, SIGTSTP); // 使用负PID表示向整个进程组发送信号
}
// return;
}

检测

1
2
make test01
make rtest01
  1. 对比两次输出,基本一样即为通过
  2. 一共要测试16个test