首页
社区
课程
招聘
[原创]【深入理解计算机系统】 手搓一个tiny shell
发表于: 2025-8-2 18:01 446

[原创]【深入理解计算机系统】 手搓一个tiny shell

2025-8-2 18:01
446

前言

此lab要求实现一个带作业控制的tiny shell,涉及到并发。推荐读者完整阅读过一次CSAPP第八章的内容再尝试此lab。

取得lab

此处取得lab文件,在此处取得官方文档。

利用命令tar xvf shlab-handout.tar解压文件。

以下所有代码皆在tsh.c中编写。

实验要求

  • 用户输入的命令行包含一个name及零或多个参数,参数由单或多个空格分隔。若name是一个内置命令,则tsh(tiny shell的简称)应立即响应并等待下条命令。否则,应将name视为可执行文件的路径,tsh应将其加载并运行在子进程上下文中。
  • 输入ctrl+c(ctrl+z)会发送一个SIGINT(SIGTSTP)到前台作业以及其子进程中。若没有任何前台作业,则信号应不产生任何效果(不能影响tsh)。
  • 若命令行末尾包含&,则将命令运行在后台,否则运行在前台。
  • 每个作业都可以用PID或JID识别。JID应用前辍%标示,PID则不用任何前辍。
  • tsh应支持以下内置命令:
    • quit:退出tsh。
    • jobs:列出所有作业。
    • bg <job>:传送给job(可用PID或JID指定)一个SIGCONT信号,并将该作业运行在后台。
    • fg <job>:传送给job(可用PID或JID指定)一个SIGCONT信号,并将该作业运行在前台。
  • tsh应回收所有僵死(尸)进程。

开始编写

以下是我们需要实现的函数:

  • eval:解析命令行。
  • built_cmd:运行内置命令。
  • do_bgfg:实现bgfg指令。
  • waitfg:挂起tsh并等待前台作业。
  • sigchld_handler:处理SIGCHLD信号。
  • sigint_handler:处理SIGINT信号。
  • sigtstp_handler:处理SIGTSTP信号。

错误处理包装函数

为了避免每次调用系统函数前手动编写错误处理代码,我们使用包装函数替代。

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
59
60
61
62
63
64
65
/* Sigprogmask - Wrapper for sigprocmask funciton */
int Sigprocmask(int how, sigset_t *set, sigset_t *oldset)
{
    int success;
    if ( (success = sigprocmask(how, set, oldset)) < 0 )
        unix_error("sigprocmask error");
    return success;
}
 
/* Fork - Wrapper for sigprocmask function */
int Fork()
{
    pid_t pid;
    if ( (pid = fork()) < 0 )
        unix_error("fork error");
    return pid;
}
 
/* Execve - Wrapper for execve function */
int Execve(const char *filename, const char **argv, const char *envp[])
{
    int success;
    if ( (success = execve(filename, argv, envp)) < 0) {
        printf("%s: Command not found\n", filename);
        exit(0);
    }
 
    return success;
}
 
/* Sigemptyset - Wrapper for sigemptyset function */
int Sigemptyset(sigset_t *set)
{
    int success;
    if ( (success = sigemptyset(set)) < 0)
        unix_error("sigemptyset error");
    return success;
}
 
/* Sigfillset - Wrapper for sigfillset function */
int Sigfillset(sigset_t *set)
{
    int success;
    if ( (success = sigfillset(set)) < 0)
        unix_error("sigfillset error");
    return success;
}
 
/* Sigaddset - Wrapper for sigaddset function */
int Sigaddset(sigset_t *set, int signum)
{
    int success;
    if ( (success = sigaddset(set, signum)) < 0)
        unix_error("sigaddset error");
    return success;
}
 
/* Kill - Wrapper for kill function */
int Kill(pid_t pid, int sig)
{
    int success;
    if ( (success = kill(pid, sig)) < 0 && errno != ESRCH)
        unix_error("kill error");
    return success;
}

eval

通过阅读提供的helper函数,我们可以知道parseline在解析命令行的同时,会将该命令是否指定运行在后台作为返回值传回。我们将返回值存放在bg中。

然后调用builtin_cmd(等下编写)确认是否为内置命令。

若不是内置命令,我们需要fork一个子进程出来,然后利用execve运行程序在其上下文中。注意,执行fork前需要先阻塞SIGCHLD信号,直到执行完addjob才恢复。避免因为竞争导致添加了无法删除的作业。想像这种流程,在子进程fork出来后直接运行,并在控制流传递给父进程前执行完毕并发送SIGCHLD信号。此时父进程收到信号并回收子进程,然后再调用addjob添加一个已经被回收的作业,导致该作业永远不会被删除。为了避免该状况,我们需要阻塞信号。

还需要注意的是,为了避免传信号给子进程时也将信号传给tsh,我们使用setpgid(0, 0)创建一个新进程组并将子进程添加进去。

最后,如果是前台进程则调用waitfg挂起tsh直到前台作业返回,否则不挂起tsh在后台运行作业。

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
/*
 * eval - Evaluate the command line that the user has just typed in
 *
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard. 
*/
void eval(char *cmdline)
{
    char *argv[MAXARGS];
    int bg = parseline(cmdline, argv);  /* background job? */
    int builtin = builtin_cmd(argv);    /* is built-in command? */
 
    sigset_t mask_one, mask_all, prev;
 
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    Sigfillset(&mask_all);
 
    if (!builtin) {
        Sigprocmask(SIG_BLOCK, &mask_one, &prev);   /* block SIGCHLD */
        pid_t pid;
 
        if ( (pid = Fork()) == 0 ) {                /* child process */
            Sigprocmask(SIG_SETMASK, &prev, NULL);  /* unblock SIGCHLD */
            setpgid(0, 0);
            Execve(argv[0], argv, environ);
        }
 
        Sigprocmask(SIG_BLOCK, &mask_all, NULL);   /* block all signals for atomic operation */
        if (bg) {
            addjob(jobs, pid, BG, cmdline);
            struct job_t *j = getjobpid(jobs, pid);
            printf("[%d] (%d) %s", j->jid, j->pid, j->cmdline);
        }
        else {
            addjob(jobs, pid, FG, cmdline);
            waitfg(pid);
        }           
        Sigprocmask(SIG_SETMASK, &prev, NULL);      /* unblock SIGCHLD */
    }
    return;
}

builtin_cmd

直接上代码,简单直接。

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
/*
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately. 
 */
int builtin_cmd(char **argv)
{
    if (argv[0] == NULL)
        return 1;
 
    int builtin = 0;
 
    char *cmdlist[4] = {"quit", "jobs", "bg", "fg"};
    int idx = -1;
 
    int i;
    for (i = 0; i < 4; i++) {
        if (strcmp(argv[0], cmdlist[i]) == 0) {
            idx = i;
            builtin = 1;
        }
    }       
 
    switch (idx) {
    case 0:
        exit(0);
    case 1:
        listjobs(jobs);
        break;
    case 2:
        do_bgfg(argv);
        break;
    case 3:
        do_bgfg(argv);
        break;
    default:
        break;
    }
 
    return builtin;
}

do_fgbg

需要注意的是,记得取得作业后,依对应命令修改作业的state。以及使用kill传送信号时,应传入-j->pid,带负号-表示将信号传送到|pid|所属的进程组中的所有进程中。

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
/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv)
{
    if (argv[1] == NULL) {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
 
    if (isalpha(*argv[1])) {
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }
 
    /* get the job who needs to continue */
    struct job_t *j;
    char *delim = strchr(argv[1], '%');
    if (delim == NULL) {            /* use PID */
        pid_t pid = atoi(argv[1]);
        j = getjobpid(jobs, pid);
 
        if (j == NULL) {
            printf("(%d): No such process\n", pid);
            return;
        }
    }
    else {                          /* use JID */
        int jid = atoi(++argv[1]);
        j = getjobjid(jobs, jid);
 
        if (j == NULL) {
            printf("%%%s: No such job\n", argv[1]);
            return;
        }
    }
 
    if (strcmp(argv[0], "bg") == 0) {                /* continue in background */
        j->state = BG;
        printf("[%d] (%d) %s", j->jid, j->pid, j->cmdline);
        Kill(-j->pid, SIGCONT);
    }
    else {                                          /* continue in foreground */
        j->state = FG;
        Kill(-j->pid, SIGCONT);
        waitfg(j->pid);
    }
    return;
}

waitfg

我们使用sigsuspend(&mask)挂起进程,其行为等同于以下代码的原子(不可中断)版本,解决了无限挂起的问题:

1
2
3
4
sigprocmask(SIG_SETMASK, &mask, &prev);
/* 若此时收到信号,可能导致进程被无限挂起 */
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    sigset_t mask;
    Sigemptyset(&mask);
     
    while (fgpid(jobs) == pid)
        sigsuspend(&mask);
 
    return;
}

sigchld_handler

需要注意,我们需要使用while而不是if来回收。这是因为必须考虑信号不会排队的问题,若有两个进程同时发送SIGCHLD信号,则只会被视为一个。如果使用if回收,则无法收回所有僵死进程。

还需要注意的是,处理函数可能会改变errno的值,所以需要在进入函数时保存旧值,并在退出时恢复。

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
/*
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate. 
 */
void sigchld_handler(int sig)
{
    int olderrno = errno;
     
    sigset_t mask, prev;
    Sigfillset(&mask);
 
    pid_t pid;
    int status;
     
    while ( (pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        Sigprocmask(SIG_BLOCK, &mask, &prev);   /* block all */
        if (WIFEXITED(status))      /* job exit */
            deletejob(jobs, pid);
        if (WIFSIGNALED(status)) {  /* job interrupt */
            printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
            deletejob(jobs, pid);
        }
        if (WIFSTOPPED(status)) {   /* job suspend */
            struct job_t *j = getjobpid(jobs, pid);
            if (j) {
                j->state = ST;
                printf("Job [%d] (%d) stopped by signal %d\n", j->jid, pid, WSTOPSIG(status));
            }
            else
                printf("cannot find a job that corresponding to pid: %d", pid);
        }   
        Sigprocmask(SIG_SETMASK, &prev, NULL);  /* unblock all */
    }
 
    /* waitpid sets errno to ECHILD if there's no more zombie process */
    /* be careful: we have to ignore EINTR too */
    if (errno != ECHILD && errno != EINTR)
        unix_error("waitpid error");
 
    errno = olderrno;
    return;
}

sigint_handler

不难,看看注释。

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
/*
 * sigint_handler - The kernel sends a SIGINT to the shell whenver the
 *    user types ctrl-c at the keyboard.  Catch it and send it along
 *    to the foreground job. 
 */
void sigint_handler(int sig)
{
    int olderrno = errno;
 
    pid_t pid = fgpid(jobs);
 
    /* we don't need to response when there's no foreground job */
    if (pid == 0) {
        errno = olderrno;
        return;
    }
     
    sigset_t mask, prev;
    Sigfillset(&mask);
     
    Sigprocmask(SIG_BLOCK, &mask, &prev);   /* block all signals */
 
    /* send signal to all the process in the same group to pid */
    Kill(-pid, sig);
 
    Sigprocmask(SIG_SETMASK, &prev, NULL);  /* unblock all signals */
 
    errno = olderrno;
    return;
}

sigtstp_handler

sigint_handler差不多。

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
/*
 * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
 *     the user types ctrl-z at the keyboard. Catch it and suspend the
 *     foreground job by sending it a SIGTSTP. 
 */
void sigtstp_handler(int sig)
{
    int olderrno = errno;
 
    pid_t pid = fgpid(jobs);
 
    if (pid == 0) {
        errno = olderrno;
        return;
    }
     
    sigset_t mask, prev;
    Sigfillset(&mask);
     
    Sigprocmask(SIG_BLOCK, &mask, &prev);   /* block all signals */
     
    Kill(-pid, sig);
     
    Sigprocmask(SIG_SETMASK, &prev, NULL);  /* unblock all signals */
 
    errno = olderrno;
    return;
}

测试

使用make编译程序,然后使用make test01make test02......测试程序。

可以使用make rtest01运行标准解答。

对比两份输出可以知道程序是否运行正确。

后记

这个lab是写的最顺的一个,因为遇到问题实在找不到bug就直接去找chatgpt,不像之前几个lab要动脑子不敢问gpt,怕他一不小心就把答案给出来。

关于各种sig_handler书中都有大差不差的代码例子,创建子进程的逻辑书中也写得很清楚,写起来很顺。

因为我是第一次写并发的代码,可能有地方写错或没考虑清楚,如果各位师傅发现任何错误欢迎指正。


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回