首页 > 编程语言 >XMU《UNIX 系统程序设计》第六次实验报告(信号处理)

XMU《UNIX 系统程序设计》第六次实验报告(信号处理)

时间:2024-04-28 09:23:19浏览次数:19  
标签:sset SIGQUIT alarm pid UNIX handler XMU 实验报告 buf

实验六 信号处理

完整程序可以在这里下载:点击下载

一、实验内容描述

实验目的

学习和掌握信号的处理方法,特别是 sigactionalarmsigpendingsigsetjmpsiglongjmp 等函数的使用。

实验要求

  1. 编制具有简单执行时间限制功能的shell:

    myshell   [ -t  <time> ]
    

    这个测试程序的功能类似实验 3,但是具有系统 shell (在 cs8 服务器上是 bash)的全部功能。<time> 是测试程序允许用户命令执行的时间限制,默认值为无限制。当用户命令的执行时间限制到达时,测试程序终止用户命令的执行,转而接收下一个用户命令。

  2. myshell 只在前台运行。

  3. Ctrl-\ 键不是中断 myshell 程序的运行,而是中断当前用户命令的接收或终止当前用户命令的执行,转而接收下一个用户命令。

  4. 注意信号 SIGALRMSIGQUIT 之间嵌套关系的处理。

  5. 2023年12月30日23:59为实验完成截止期。

二、设计与实现

由于我们没有做实验三,因此这个实验其实是两个任务的合并。第一个任务是实现一个 shell,第二个任务是实现时间限制功能。

实现 shell

比起实验三,这个实验有关 shell 的任务要容易得多。因为不需要像实验三那样去模拟命令的执行,只需要按照提示使用系统 /bin/sh 来调用即可。

execl("/bin/sh", "sh", "-c", buf, (char *) 0)

命令提示符的打印和命令的读取

这一步很简单,只需要打印一个 > 作为命令提示符,然后等待用户输入命令即可。

我使用 fgets 来读入用户输入的命令,然后调用 eval 函数来处理有关命令执行的事情。

// in main
char cmdline[MAXLINE];
while (1) {
    sigsetjmp(jmpbuf, 1);

    printf("> ");
    fgets(cmdline, MAXLINE, stdin);
    if (feof(stdin)) exit(0);
    eval(cmdline);
}

这里如果读取到了 EOF,那我就让这个 shell 退出(注意要求:要有系统 shell 的全部功能,因此这个细节也要实现上去),这样就可以通过 Ctrl-D 来退出 shell 了。

命令执行

我在 eval 函数中执行命令。函数接受命令内容为 cmdline 参数。

首先为了确保执行结果不出错,我将 cmdline 中头尾的空白字符全部删除,其结果我用 buf 变量表示:

char *buf = cmdline;
while (*buf != 0 && isspace(*buf)) ++buf;
for (char *p = buf + strlen(buf) - 1; p >= buf && isspace(*p); --p) *p = 0;

然后,我会调用 fork 来创建一个子进程,在子进程中,我使用 execl("/bin/sh", "sh", "-c", buf, (char *) 0) 来执行这个命令,并做相关的错误处理。

值得注意的是,这里的 pid 是一个全局变量,而且使用 volatile 修饰,以便在后续信号处理中可以直接判断当前执行的进程。

if ((pid = Fork()) == 0) {
    if (execl("/bin/sh", "sh", "-c", buf, (char *) 0) < 0) {
        printf("%s: Command not found.\n", buf);
        exit(0);
    }
}

接着,使用 waitpid(pid, &status, 0) 来等待子进程退出。如果这个函数的返回值小于零,说明子进程退出异常。这可能是因为在这个函数被调用前子进程就已经退出了(ECHILD),还有可能是子进程是由于后面会加上的 alarm 导致系统调用被打断而退出,这种情况下 errnoEINTR。我对这两种异常退出进行了特判,其余的异常则进行报错:

int status;
if (waitpid(pid, &status, 0) < 0) {
    if (errno != ECHILD && errno != EINTR)
        fprintf(stderr, "waitpid error: %s\n", strerror(errno));
}

随后,将 pid 设置为 \(0\),代码当前没有命令在执行。

quitexit 的特殊处理

现在我们的模拟 shell 已经可以执行很多命令了,但是部分和 shell 本身相关的命令还没法正确执行。

其中就包括 quit/exit 这一组用来退出 shell 的命令,以及 cd 这个用来切换工作目录的命令。

对于 quitexit 的判断很简单,只要 buf 的内容是 quit 或者 exit 我们就退出程序。

if (!strcmp(buf, "quit") || !strcmp(buf, "exit"))
    exit(0);

cd 的特殊处理

/bin/sh 中执行 cd 并不会改变父进程 myshell 的工作目录,因此我们要直接在父进程切换工作目录。

这里可以使用 chdir 系统调用来实现。

具体地,首先判断命令是否为 cd ... 或者 cd 这样的命令。如果是单个 cd 就直接切换到 HOME 目录;如果还接了字符,那么就切换到后续字符对应的目录中。

if (!strncmp(buf, "cd", 2) && (buf[2] == 0 || isspace(buf[2]))) {
    if (buf[2] == 0) {
        Chdir(getenv("HOME"));
    } else {
        char *path = buf + 3;
        while (*path != 0 && isspace(*path)) ++path;
        Chdir(path);
    }
    return;
}

最终的 eval 函数

void eval(char *cmdline) {
    char *buf = cmdline;
    while (*buf != 0 && isspace(*buf)) ++buf;
    for (char *p = buf + strlen(buf) - 1; p >= buf && isspace(*p); --p) *p = 0;
    
    if (!strcmp(buf, "quit") || !strcmp(buf, "exit"))
        exit(0);

    if (!strncmp(buf, "cd", 2) && (buf[2] == 0 || isspace(buf[2]))) {
        if (buf[2] == 0) {
            Chdir(getenv("HOME"));
        } else {
            char *path = buf + 3;
            while (*path != 0 && isspace(*path)) ++path;
            Chdir(path);
        }
        return;
    }

    if (timelimit) alarm(timelimit);

    if ((pid = Fork()) == 0) {
        if (execl("/bin/sh", "sh", "-c", buf, (char *) 0) < 0) {
            printf("%s: Command not found.\n", buf);
            exit(0);
        }
    }

    int status;
    if (waitpid(pid, &status, 0) < 0) {
        if (errno != ECHILD && errno != EINTR)
            fprintf(stderr, "waitpid error: %s\n", strerror(errno));
    }

    if (timelimit) alarm(0);
    pid = 0;
}

实现时间限制

参数判断

首先当然是要正确地读取命令行参数来获得时间限制啦。

if (argc != 1 && (argc != 3 || strcmp(argv[1], "-t"))) {
    printf("Usage: %s [-t <time>]\n", argv[0]);
    exit(0);
}
if (argc == 3 && (timelimit = atoi(argv[2])) <= 0) {
    printf("<time> should be a positive integer.\nUsage: %s [-t <time>]\n", argv[0]);
    exit(0);
}

eval 中调用 alarm

很容易想到的思路是,在 fork 进行之前,调用 alarm 设置时间限制。在 waitpid 返回后,也就说明子进程结束了,那么我们就再调用 alarm 取消时间限制。

// in eval function
if (timelimit) alarm(timelimit);

if ((pid = Fork()) == 0) {
    if (execl("/bin/sh", "sh", "-c", buf, (char *) 0) < 0) {
        printf("%s: Command not found.\n", buf);
        exit(0);
    }
}

int status;
if (waitpid(pid, &status, 0) < 0) {
    if (errno != ECHILD && errno != EINTR)
        fprintf(stderr, "waitpid error: %s\n", strerror(errno));
}

if (timelimit) alarm(0);
pid = 0;

理清 SIGALRMSIGQUIT 的嵌套关系

SIGALRMalarm 设置的定时时间到达的信号,而 SIGQUIT 是手动 Ctrl-\ 产生的信号。

首先,很重要的一点是,在执行其中任意一个信号的处理函数的过程中,都不能接受另一个信号的处理函数。因为任意一个信号的处理函数,都会关闭当前执行的进程,而我们不需要重复关闭。因此,这两个信号的处理函数执行过程中,都必须屏蔽另一个信号。

假设在一个信号处理函数的执行过程中屏蔽了另一个信号,但是屏蔽过程中另一个信号被发出,那么会在解除屏蔽后立刻执行另一个信号的信号处理函数。那么这是不是我们希望的呢?

如果一个在处理 SIGALRM 信号的过程中,用户手动通过 Ctrl-\ 发出了 SIGQUIT 信号,那么我们其实是不需要在 alarm 处理后再调用 SIGQUIT 处理函数重新关闭进程的,因此此时我们应该忽略未决的 SIGQUIT 信号。

如果一个在处理 SIGQUIT 信号的过程中,收到了 SIGALRM 信号,我们也是不需要再 SIGQUIT 关闭子进程后重新关闭的,因此我们也应该忽略未决的 SIGQUIT 信号。

也就是说,在任意一个信号处理函数解除对另一个信号的屏蔽之前,都应该忽略掉此时未决的另一个信号。

值得注意的是,如果在 SIGQUIT 函数,那么会关闭当前命令的子进程,而当前命令可能会带着 alarm 设置的定时,应该要把这个 alarm 清空。

设置 SIGALRMSIGQUIT 的信号处理函数

我会使用 alarm_handlerquit_handler 来分别处理 SIGALRMSIGQUIT 两个信号,因此我要在 shell 打印命令提示符之前设置它们为对应的信号处理函数。

sigset_t sset;
sigemptyset(&sset), sigaddset(&sset, SIGQUIT);
Signal_mask(SIGALRM, alarm_handler, sset);
sigemptyset(&sset), sigaddset(&sset, SIGALRM);
Signal_mask(SIGQUIT, quit_handler, sset);
// in main, befor while

Signal_mask 是我编写的函数,其作用是不仅可以设置信号对应的处理函数,还能设置处理函数执行过程中屏蔽的信号。其原型为:

void (*Signal_mask(int signo, void (*func)(int), sigset_t mask))(int) {
    struct sigaction act, oact;
    act.sa_handler = func;
	act.sa_mask = mask;
    act.sa_flags = 0;
    if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
#endif
    } else {
#ifdef SA_RESTART
        act.sa_flags |= SA_RESTART;
#endif
    }

    if (sigaction(signo, &act, &oact) < 0) {
        perror("sigaction error");
        exit(0);
    }
    return oact.sa_handler;
}

这里,我设置在 alarm_handler 执行过程中,屏蔽 SIGQUIT 信号;在 quit_handler 执行过程中,屏蔽 SIGALRM 信号。

alarm_handler

alarm_handler 做的事情很简单。

一旦调用 alarm_handler,只可能是定时器时间到了,那么只需要判断如果当前有正在执行的命令,就可以终止掉。

if (pid > 0) {
    kill(-pid, SIGKILL);
    printf("%d: Time limit Exceeded.\n", pid);
}
pid = 0;

在终止掉子进程之后,还应该记得设置 pid = 0,代表当前没有正在执行的子进程。

如上面分析的,在 alarm_handler 退出之前,要忽略掉所有的未决的 SIGQUIT 信号。可以如下处理:

sigset_t pendmask;
sigemptyset(&pendmask);
sigpending(&pendmask);
if (sigismember(&pendmask, SIGQUIT)) {
    Signal(SIGQUIT, SIG_IGN);
    sigset_t sset;
    sigemptyset(&sset), sigaddset(&sset, SIGALRM);
    Signal_mask(SIGQUIT, quit_handler, sset);
}

在这个代码中,我们调用 sigpending 获取了当前未决的信号,判断 SIGQUIT 信号是否在其中,然后通过调用 Signal(SIGQUIT, SIG_IGN)SIGQUIT 信号的处理函数为空(即忽略信号),来忽略掉未决的 SIGQUIT 信号,随后又调用 Signal_mask(SIGQUIT, quit_handler, SIGALRM) 来重新设置 SIGQUIT 信号的处理函数。

完整的 alarm_handler 如下:

void alarm_handler(int sig) {
    if (pid > 0) {
        kill(-pid, SIGKILL);
        printf("%d: Time limit Exceeded.\n", pid);
    }
    pid = 0;
    sigset_t pendmask;
    sigemptyset(&pendmask);
    sigpending(&pendmask);
    if (sigismember(&pendmask, SIGQUIT)) {
        Signal(SIGQUIT, SIG_IGN);
        sigset_t sset;
        sigemptyset(&sset), sigaddset(&sset, SIGALRM);
        Signal_mask(SIGQUIT, quit_handler, sset);
    }
}

初始的 quit_handler

quit_handleralarm_handler 类似,只是在 pid = 0 前后还应该调用 alarm(0) 清除掉定时器。

void quit_handler(int sig) {
    if (pid > 0) {
        kill(-pid, SIGQUIT);
        printf("%d: Quit.\n", pid);
    }
    pid = 0;
    alarm(0);
    sigset_t pendmask;
    sigemptyset(&pendmask);
    sigpending(&pendmask);
    if (sigismember(&pendmask, SIGALRM)) {
        Signal(SIGQUIT, SIG_IGN);
        sigset_t sset;
        sigemptyset(&sset), sigaddset(&sset, SIGQUIT);
        Signal_mask(SIGQUIT, alarm_handler, sset);
    }
}

中断当前用户命令的接收

根据实验要求:

Ctrl-\ 键不是中断 myshell 程序的运行,而是中断当前用户命令的接收或终止当前用户命令的执行,转而接收下一个用户命令。

因此,在 quit_handler 中,不应该仅仅关闭子进程。

在没有子进程(pid == 0)的时候,说明现在正在输入命令,那么此时我们应该让程序忽略掉这一行命令的接收,重新开始接受下一行命令。

这里可以使用 siglongjmp 实现。

我们在 main 函数中,每次 while 循环开始前,调用 sigsetjmp 设置一次 siglongjmp 的返回点:

while (1) {
    sigsetjmp(jmpbuf, 1);

    printf("> ");
    fgets(cmdline, MAXLINE, stdin);
    if (feof(stdin)) exit(0);
    eval(cmdline);
}

注意,sigsetjmp 的第二个参数是 \(1\),代表每次 jmp 到这里的时候,应该恢复 set 时的屏蔽信号集。

然后,我们需要修改一下 quit_handler。在这个函数中,当我们判断到 pid == 0 时,就调用 siglongjmp 跳转到循环开始。为了让 pid 不被影响,我将 pid = 0 的修改移动到了函数结束:

void quit_handler(int sig) {
    if (pid > 0) {
        kill(-pid, SIGQUIT);
        printf("%d: Quit.\n", pid);
    }
    alarm(0);
    sigset_t pendmask;
    sigemptyset(&pendmask);
    sigpending(&pendmask);
    if (sigismember(&pendmask, SIGALRM)) {
        Signal(SIGQUIT, SIG_IGN);
        sigset_t sset;
        sigemptyset(&sset), sigaddset(&sset, SIGQUIT);
        Signal_mask(SIGQUIT, alarm_handler, sset);
    }
    if (pid == 0) {
        putchar('\n');
        siglongjmp(jmpbuf, 1);
    }

    pid = 0;
}

值得一提的是,我在 siglongjmp 之前还打印了一个换行符,因为直接直接跳转的话,下一次的输入会黏在上一行的末尾,很难看。

三、实验结果

1. 编译程序

gcc myshell.c -o myshell

2. 运行截图

正常运行命令:

./实验 6 报告/image-20231215175525252

测试时间限制:

./实验 6 报告/image-20231215175618452

卡着时间限制 Ctrl-\

./实验 6 报告/image-20231215175700085

打断用户输入:

./实验 6 报告/image-20231215175743452

四、实验体会

这次实验是一次对信号处理和 shell 构建的深入探索。任务很明确:实现一个带有执行时间限制功能的 shell,这对我来说是一个挑战,因为它要求我熟练使用 sigactionalarmsigpendingsigsetjmpsiglongjmp 等函数,同时合并两个任务:一个是实现一个简单的 shell,另一个是给 shell 添加执行时间限制的功能。

首先,我专注于构建一个基本的 shell。相较于先前的实验,这次任务要简单得多。通过使用系统的 /bin/sh 来调用命令,而无需模拟命令的执行,我能够完成命令提示符的打印和命令的读取。这一步非常简单,只需使用 fgets 来读取用户输入的命令,然后调用 eval 函数来处理这些命令。

eval 函数中,我处理了命令的执行。为了保证执行结果正确,我删除了命令中头尾的空白字符,并在子进程中使用 execl("/bin/sh", "sh", "-c", buf, (char *) 0) 来执行这些命令,并进行相关的错误处理。此外,我还特殊处理了 quit/exitcd 这两个与 shell 相关的命令,确保其正确执行。

但是,实验的关键部分在于实现时间限制功能。我首先处理了命令行参数的判断,并在 eval 中调用 alarm 设置时间限制。同时,我理清了 SIGALRMSIGQUIT 两个信号的嵌套关系。在 alarm_handlerquit_handler 函数中,我处理了这两个信号的情况,保证在信号处理函数执行过程中不会被另一个信号的处理函数打断,从而确保程序的稳定性。

另外,在实验中,我实现了中断当前用户命令的接收的功能。当用户手动中断命令接收时,通过 siglongjmp 跳转到循环开始,重新开始接收下一个用户命令。

在测试阶段,我展示了程序的正常运行、时间限制、中断命令和中断用户输入等情况的截图,证明了程序在各种情况下的有效运行。

总体而言,这次实验提供了一个很好的机会,让我更深入地理解了信号处理和 shell 构建的原理。我学会了如何使用信号来控制程序的执行,以及如何处理不同信号之间的交互关系。这次实验也增强了我对系统编程的理解,让我更加熟悉和自信地处理类似任务。

标签:sset,SIGQUIT,alarm,pid,UNIX,handler,XMU,实验报告,buf
From: https://www.cnblogs.com/hankeke303/p/18162994/XMU-UNIX-Lab6

相关文章

  • XMU《计算机网络与通信》第二次实验报告
    实验二实验报告一、个人信息姓名:XXX学号:XXXXXXXXXXXXXX二、实验目的学习捕获和分析网络数据包掌握以太网MAC帧、802.11数据帧和IPv4数据包的构成,了解各字段的含义掌握ICMP协议,ping和tracert指令的工作原理掌握ARP协议的请求/响应机理三、实验内容与结果分析......
  • XMU《计算机网络与通信》第四次实验报告
    计算机网络实验四通信这次实验的话,我的报告参考意义不大,毕竟这次实验的主要难点是完成那两个代码,但是我报告中没有任何对于代码的解释。大家如果需要的话,我的两个代码可以在这里下载,仅供参考:点击下载。一、个人信息姓名:XXX学号:XXXXXXXXXXXXXX二、实验目的理解和掌握ARP......
  • XMU《计算机网络与通信》第五次实验报告
    实验五运输层与应用层协议分析如果需要Wireshark捕获到的数据,可以在这里下载,这里面应该还有最后一个任务的两个代码:点击下载。目录实验五运输层与应用层协议分析一、个人信息二、实验目的三、实验内容、步骤与结果任务一TCP正常连接观察任务二异常传输观察分析1.尝试连......
  • 20211317 李卓桐 Exp5 信息搜集与漏洞扫描 实验报告
    Exp5信息搜集与漏洞扫描实验报告1、实践目标掌握信息搜集的最基础技能与常用工具的使用方法。2、实践内容(1)各种搜索技巧的应用(2)DNSIP注册信息的查询(3)基本的扫描技术:主机发现、端口扫描、OS及服务版本探测、具体服务的查点(以自己主机为目标)(4)漏洞扫描:会扫,会看报告,会查漏......
  • inode(index node)是Unix、Linux和类Unix操作系统中的一个重要概念, 在Windows操作系统中
    inode(indexnode)是Unix、Linux和类Unix操作系统中的一个重要概念,用于描述文件系统中的文件或目录。每个文件或目录都与一个inode相关联。inode包含以下信息:文件或目录的权限(读、写、执行)。文件类型(普通文件、目录、符号链接等)。拥有者和所属组。文件的大小。访问、修......
  • delphi Unix时间戳 转yyyy-mm-dd hh:mm:ss 格式字符串
    functionUnixTimeStampToDateTimeStr(UnixTimeStamp:Int64):string;varDateTimeValue:TDateTime;begin//第二个参数默认为true,设置为false,会默认以本地时区来+8小时,因为mysql里村的utc时间秒数DateTimeValue:=UnixToDateTime(UnixTimeStampdiv1000,False......
  • XMU《UNIX 系统程序设计》第二次实验报告
    一、实验内容描述实验目的掌握与文件和目录树有关的系统调用和库函数。实验要求编写程序myfind命令语法myfind<pathname>[-comp<filename>|-name<str>...]命令语义(1)myfind<pathname>的功能除了具有与程序4-7相同的功能外,还要输出在<pathname>目录子树之下,文......
  • XMU《计算机网络与通信》第三次实验报告
    一、个人信息学号:**************姓名:###二、实验目的理解TCP和UDP协议主要特点掌握socket的基本概念和工作原理,编程实现socket通信三、实验任务与结果任务1前置任务开启两个终端窗口,分别编译、运行server_example.c和client_example.c,观察它们实现的功能。......
  • 20231325 贾罗祁 实验三《Python程序设计》实验报告
    20231325贾罗祁2023-2024-2《Python程序设计》实验三报告课程:《Python程序设计》班级:2313姓名:贾罗祁学号:20231325实验教师:王志强实验日期:2024年4月17日必修/选修:公选课1.实验内容创建服务端和客户端,服务端在特定端口监听多个客户请求。客户端和服务端通过Socket套......
  • unix /var/run/docker.sock: connect: permission denied
    由于安全整改,root被ssh_config禁用,故使用普通用户执行脚本,执行脚本过程中,使用普通用户运行docker命令时发现权限不够,报错如下GotpermissiondeniedwhiletryingtoconnecttotheDockerdaemonsocketatunix:///var/run/docker.sock:Gethttp://%2Fvar%2Frun%2Fdocker.soc......