练习1-7 编写一个打印打印EOF值的程序

本文通过一个简单的C语言程序演示了如何使用getchar()函数读取字符并判断EOF的输入方式。通过对不同字符输入的测试,包括普通字符、空格、制表符、换行符以及特殊字符,验证了在C语言中EOF的默认输入方式为Ctrl+Z。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

/*
	时间:2017/8/2
	地点:工程北629
	功能:打印EOF的值
	思想:使用printf函数
             注意EOF的输入方式是: Ctrl + Z
*/

#include<stdio.h>

int main(void)
{
	int c;

	printf("请输入字符:\n");
	c = (getchar() != EOF);  //getchar函数位于标准头文件#include<stdio.h>中
	printf("%d\n",c);
	
    return 0;
}

/*
	在Vc++6.0中的输出结果是:
----------------------------
	请输入字符:
	nicosmos
	1
	Press any key to continue

    请输入字符:
        EOF
        1
        Press any key to continue

	请输入字符:
	1234567890
	1
	Press any key to continue

	请输入字符:  //这里输入的是空格

	1
	Press any key to continue

	请输入字符:  //这里输入的是制表符

	1
	Press any key to continue


	请输入字符:  //这里输入的是换行符,直接点击Enter即可

	1
	Press any key to continue

	请输入字符:  //这里输入的方式是:直接点击Ctrl + Z
	^Z
	0
	Press any key to continue

----------------------------
  总结:1、计算机是对字符进行处理的。
       2、字符:数字字母(0,1,a,B,..)、空白符(空格,制表符,换行符)、特殊字符(%,&...)
       3、除Ctrl + Z结果为0,其余全部是1,说明EOF是Ctrl+Z方式输入的。
*/



<think>我们计划编写一个简易的Shell程序,需要支持以下功能: 1. 执行外部命令:通过创建子进程,使用exec系列函数执行用户输入的命令。 2. 传递参数:将用户输入的命令和参数解析成字符串数组,传递给exec函数。 3. 处理输入和输出:接收命令的输入,并打印命令的输出(这里我们暂时使用标准输入输出)。 4. 内置命令处理:`stop`命令用于终止当前正在运行的命令进程(如果有的话),`exit`命令用于退出Shell程序。 5. 注意:对于像`cd`这样的内建命令,需要由Shell本身(父进程)执行,因为改变目录的操作如果由子进程执行,不会影响父进程。 根据引用[2]和[3],我们知道: - 对于非内建命令,我们创建子进程去执行,父进程等待子进程结束(除非要求后台运行,但题目没有要求,所以我们默认等待)。 - 对于内建命令(如`cd`),我们需要在父进程中直接执行。另外,题目要求的内建命令是`stop`和`exit`,所以我们也需要将它们作为内建命令处理。 设计思路: 1. 主循环:不断读取用户输入的命令行。 2. 解析命令行:将命令行分割成命令和参数。 3. 判断命令类型: - 如果是内建命令(`exit`, `stop`, `cd`等),则直接在父进程中处理。 - 否则,创建子进程执行外部命令,父进程等待子进程结束(除非要求后台运行,但本题不要求后台,所以等待)。 关于`stop`命令:我们需要能够终止当前正在运行的命令进程。但是,我们的设计是父进程等待子进程结束,所以同一时间只有一个命令在运行(前台运行)。因此,`stop`命令实际上就是发送信号给当前正在运行的子进程(如果有的话)。然而,在父进程等待子进程的时候,它是阻塞的,所以无法在等待的同时接收新的命令(比如`stop`)。因此,我们需要重新考虑。 解决方案: - 我们可以让父进程不等待子进程,而是记录当前正在运行的子进程的PID,然后继续循环。但是这样会带来新的问题:用户可能同时输入多个命令,导致多个子进程同时运行,而我们只记录最后一个子进程的PID。而且,题目要求输入`stop`时终止命令进程,所以我们需要记录这个PID。 - 另一种方法是使用异步等待:父进程在创建子进程后,不阻塞等待,而是继续循环,但通过信号处理来回收子进程。同时,我们记录当前前台子进程的PID,这样当用户输入`stop`时,我们可以向该PID发送终止信号。 但是,题目要求的功能中,`stop`命令用于终止命令进程。我们假设一次只运行一个命令(即前台命令),那么我们可以这样设计: - 父进程在创建子进程后,记录子进程的PID(设为`current_pid`),然后等待该子进程结束(使用`waitpid`)。这样,在等待期间,父进程被阻塞,无法响应其他命令。因此,用户无法在命令运行期间输入`stop`(除非我们允许后台运行,或者使用非阻塞方式)。 考虑到题目要求,我们可能需要支持在命令运行过程中,通过另一个终端或者信号来终止?但题目没有明确。根据要求,输入`stop`时终止命令进程,这意味着我们需要在命令运行时能够接收用户的输入(即`stop`命令)。因此,我们可能需要将命令运行在后台,然后父进程继续等待用户输入。但是这样会使得Shell能够同时运行多个命令,而我们只记录最后一个命令的PID,这样`stop`只能终止最后一个命令。 为了简化,我们假设一次只运行一个命令(前台命令),并且我们不支持在命令运行过程中输入其他命令(包括`stop`)。那么,`stop`命令只能在命令没有运行时使用?这显然不合理。 重新理解题目:输入`stop`时终止命令进程。这里可能是指终止由当前Shell启动的后台命令?但题目没有明确后台运行。所以,我们可能需要支持后台运行命令(在命令末尾添加`&`)?但是题目也没有要求。 考虑到题目要求的功能,我们可能需要这样设计: - 如果用户输入的命令不是内建命令,则创建子进程执行,并且不等待其结束,而是将其放入后台运行,并记录其PID。 - 父进程立即返回,继续读取用户输入。 - 当用户输入`stop`时,我们向最近一个后台进程发送终止信号(或者可以指定PID?但题目没有要求,所以我们就终止最近启动的后台进程)。 但是,题目要求的功能描述中并没有提到后台运行,而且要求“在输入stop时终止命令进程”。另外,题目还要求“接收并打印命令的输入内容”,这通常是指命令的输出,所以我们希望命令在前台运行,这样我们才能看到输出。 因此,我们可能需要使用非阻塞的方式:在父进程中,创建子进程后,不等待,而是记录该子进程的PID,然后继续循环。但是,为了能够打印命令的输出,我们需要让命令在前台运行(即控制终端的前台进程组)。同时,父进程需要能够同时等待用户输入和子进程结束。这可以通过多进程或者异步I/O来实现,但比较复杂。 另一种思路:使用两个进程,一个用于等待用户输入,一个用于执行命令并等待命令结束。但这会增加复杂性。 考虑到这是一个简易Shell,我们简化需求:一次只允许运行一个命令(前台命令),在命令运行期间,Shell不能接受新的命令。这样,`stop`命令只能在命令没有运行时使用,用于终止后台命令?但我们的设计没有后台命令。所以,我们可能需要重新考虑`stop`的功能。 题目要求:在输入`stop`时终止命令进程。这个命令进程可能是之前启动的后台命令?但题目没有明确。所以,我们假设用户可以在命令运行过程中输入`stop`来终止当前正在运行的前台命令。但是,在命令运行期间,父进程正在等待子进程,所以无法接收新的输入。 为了解决这个问题,我们可以使用信号: - 在父进程中,当启动子进程后,父进程进入等待状态,但是我们可以设置一个信号处理函数,当父进程收到SIGINT(比如Ctrl+C)或者自定义信号时,可以终止子进程。但是,题目要求输入`stop`命令,而不是通过信号(如Ctrl+C)。 因此,我们需要在父进程等待子进程的同时,还能够读取用户输入(即`stop`命令)。这可以通过多线程实现:一个线程等待子进程结束,另一个线程等待用户输入。但多线程可能会增加复杂性。 另一种方法是使用`select`或`poll`等I/O多路复用来同时监视子进程状态和标准输入。但是,等待子进程结束通常使用`waitpid`,它是阻塞的,不会和文件描述符一起被`select`监视。 我们可以使用非阻塞的`waitpid`,然后在一个循环中同时检查输入和子进程状态。但是,这需要将子进程的等待改为非阻塞,并且循环检查。 具体步骤: 1. 创建子进程执行命令。 2. 在父进程中,记录子进程PID(current_pid),然后进入循环: a. 使用`select`监视标准输入(stdin)是否有数据(用户输入了命令)。 b. 同时,使用非阻塞`waitpid`检查子进程是否结束。 c. 如果用户输入了命令,读取输入,判断是否为`stop`,如果是,则向current_pid发送SIGTERM信号,然后跳出循环(或者继续等待子进程退出)。 d. 如果子进程退出,则跳出循环。 但是,这样我们就需要在一个循环中同时处理用户输入和子进程状态,而且命令的输出可能会和Shell的提示符混在一起,因为命令的输出和Shell的输入是并行的。 考虑到复杂度,并且题目要求的是一个简易Shell,我们可以选择不支持在命令运行过程中输入`stop`来终止。而是这样设计: - 如果用户想要终止一个正在运行的前台命令,可以使用Ctrl+C(发送SIGINT),这通常能终止前台进程组(包括子进程)。然后,我们的Shell捕获SIGINT并传递给子进程,或者忽略(由子进程处理)。这是常见Shell的做法。 - 而`stop`命令我们设计为:如果当前有后台进程运行,则终止最近的一个后台进程;如果没有,则提示无进程可终止。但是题目没有要求后台运行,所以我们可以不支持后台运行,那么`stop`命令就没有可终止的进程。 因此,我们需要重新审视题目要求:“在输入 stop 时终止命令进程”。这里可能是指终止之前由Shell启动的后台命令?但题目没有提到后台运行。所以,我们可能误解了。 另一种解释:`stop`命令用于终止Shell本身启动的命令进程,但仅限于当前正在运行的命令。由于我们默认是前台运行,所以无法在命令运行期间输入`stop`。因此,我们只能将`stop`设计为终止后台进程。这就需要我们支持后台运行。 我们决定:支持后台运行命令(在命令末尾添加`&`),这样父进程不等待子进程,而是记录子进程的PID(放入一个列表),然后继续接受命令。当用户输入`stop`时,我们终止最近启动的后台进程(或者所有后台进程?但题目没有说明,我们就终止最近的一个)。 同时,对于前台命令(没有`&`),父进程等待子进程结束,期间不能接受新命令。 这样,`stop`命令只能终止后台进程。 总结功能: 1. 读取命令行:显示提示符,读取用户输入。 2. 解析命令行:分割字符串,判断是否有后台运行符`&`。 3. 内建命令: - `exit`:退出Shell。 - `stop`:终止最近启动的后台进程(如果有的话)。 - `cd`:改变当前目录(内建命令)。 4. 外部命令: - 如果有`&`,则在后台运行:创建子进程,执行命令,父进程记录子进程PID(用一个变量记录最近的后台进程PID,或者用一个列表记录所有后台进程,但我们只终止最近的一个,所以用一个变量`last_background_pid`即可),然后父进程继续循环。 - 否则,前台运行:创建子进程,执行命令,父进程等待子进程结束,然后继续循环。 注意:后台进程结束需要回收,避免僵尸进程。我们可以通过忽略SIGCHLD信号,或者设置回收。这里我们选择在父进程中定期回收(使用非阻塞waitpid)或者设置信号处理。 另外,对于`stop`命令,我们向记录的后台进程PID发送SIGTERM信号。 实现步骤: 1. 主循环: - 打印提示符(如`mysh>$ `) - 读取一行输入 - 解析输入(去掉换行符,分割参数,检查末尾是否有`&`) 2. 判断命令: - 如果是空行,继续循环。 - 如果是`exit`,则退出循环,结束Shell。 - 如果是`stop`,则查找最近的后台进程PID,发送SIGTERM,然后继续循环。 - 如果是`cd`,则调用`chdir`函数,并继续循环。 - 否则,为外部命令。 3. 外部命令处理: - 创建子进程。 - 在子进程中:使用`execvp`执行命令(注意,命令可以是带路径的,也可以是文件名,由环境变量PATH查找)。 - 在父进程中: - 如果是前台命令(没有`&`),则等待子进程结束(使用`waitpid`)。 - 如果是后台命令(有`&`),则记录子进程的PID(`last_background_pid = pid`),并不等待,继续循环。同时,为了避免僵尸进程,我们可以设置SIGCHLD信号处理函数,在信号处理中回收后台进程。 4. 后台进程回收: - 注册SIGCHLD信号处理函数,在该函数中,使用`waitpid(-1, NULL, WNOHANG)`来回收任意结束的子进程。注意,由于可能有多个子进程同时结束,所以需要使用循环。 5. 注意:`last_background_pid`记录的是最近启动的后台进程,即使它已经结束,我们仍然记录着它的PID(可能已经无效)。因此,在`stop`命令中,我们发送信号前需要检查该进程是否还存在。但是,由于进程结束已经被我们回收,所以我们可以通过检查`kill(pid, 0)`的返回(如果返回0表示存在,-1且errno==ESRCH表示不存在)。另外,如果进程已经结束,我们就不需要发送信号了。 但是,我们回收后台进程时,会将其从进程表中移除,所以当`stop`时,如果进程已经结束,我们就不需要发送信号。同时,我们记录`last_background_pid`,但有可能该进程已经被回收,所以我们需要在发送信号前检查进程是否存在。 然而,在回收时,我们并不知道哪个后台进程结束了,所以`last_background_pid`可能指向一个已经不存在的进程。因此,在`stop`命令中,我们尝试向`last_background_pid`发送信号,如果失败(且errno为ESRCH),则说明进程已经结束,我们只需提示用户。 另外,我们只记录最近的一个后台进程,所以如果有多个后台进程,我们只能终止最近启动的那个。 代码结构: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #include <errno.h> #define MAX_INPUT_LENGTH 1024 #define MAX_ARGS 64 pid_t last_background_pid = -1; // 记录最近后台进程的PID // SIGCHLD信号处理函数:回收后台子进程 void sigchld_handler(int sig) { pid_t pid; int status; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { // 如果回收的进程是最近的后台进程,并且已经结束,我们可以重置last_background_pid吗? // 注意:可能有多个后台进程,我们只记录最近的一个,所以这里不重置last_background_pid,因为可能还有其他的后台进程,但我们的设计只记录最近的一个,所以即使它结束了,我们仍然保留它的PID(直到有新的后台进程覆盖) // 实际上,我们并不需要重置,因为当需要stop时,我们会检查进程是否存在。 } } // 执行命令 void execute_command(char **args, int background) { pid_t pid = fork(); if (pid == 0) { // 子进程 execvp(args[0], args); perror("execvp"); exit(EXIT_FAILURE); } else if (pid < 0) { perror("fork"); } else { // 父进程 if (background) { printf("[%d] running in background\n", pid); last_background_pid = pid; // 记录后台进程PID } else { // 前台进程,等待 int status; waitpid(pid, &status, 0); } } } int main() { char input[MAX_INPUT_LENGTH]; char *args[MAX_ARGS]; char *token; int background; // 设置SIGCHLD信号处理 struct sigaction sa; sa.sa_handler = sigchld_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(EXIT_FAILURE); } while (1) { background = 0; printf("mysh> "); fflush(stdout); if (fgets(input, MAX_INPUT_LENGTH, stdin) == NULL) { break; // 遇到EOF(Ctrl+D) } // 去掉换行符 input[strcspn(input, "\n")] = '\0'; // 解析输入 int i = 0; token = strtok(input, " "); while (token != NULL && i < MAX_ARGS - 1) { args[i++] = token; token = strtok(NULL, " "); } args[i] = NULL; // 数组以NULL结尾 // 空命令 if (i == 0) { continue; } // 检查是否有后台运行符(&),且只能出现在最后一个参数 if (i > 0 && strcmp(args[i-1], "&") == 0) { background = 1; args[i-1] = NULL; // 去掉& // 如果去掉&后,命令为空,则继续 if (i-1 == 0) { continue; } } // 内建命令 if (strcmp(args[0], "exit") == 0) { break; } else if (strcmp(args[0], "cd") == 0) { if (args[1] == NULL) { fprintf(stderr, "cd: missing argument\n"); } else { if (chdir(args[1]) != 0) { perror("cd"); } } } else if (strcmp(args[0], "stop") == 0) { if (last_background_pid != -1) { // 检查进程是否存在 if (kill(last_background_pid, 0) == 0) { // 发送终止信号 if (kill(last_background_pid, SIGTERM) == 0) { printf("Stopped [%d]\n", last_background_pid); } else { perror("kill"); } } else { printf("No background process running\n"); } } else { printf("No background process running\n"); } } else { // 外部命令 execute_command(args, background); } } return 0; } ``` 注意: 1. 信号处理函数`sigchld_handler`中,我们使用`waitpid`循环回收所有已经结束的子进程,避免僵尸进程。 2. 对于`stop`命令,我们只终止最近的后台进程,并且先检查该进程是否存在(通过`kill(pid,0)`)。 3. 后台进程的输出可能会和Shell的提示符混在一起,因为父进程在后台进程运行的同时继续运行,打印提示符。我们可以通过更复杂的输出控制来解决,但简易Shell可以忽略。 4. 内建命令`cd`只支持一个参数(不支持多个参数,不支持`~`扩展),这是为了简化。 5. 对于`stop`命令,我们发送SIGTERM信号,这会让进程终止。如果进程不处理SIGTERM,可能需要发送SIGKILL(`kill -9`),但题目没有要求,我们使用SIGTERM。 这个简易Shell满足了题目的基本要求。但是,它不支持管道、重定向等高级功能,只支持简单的命令执行。 注意:在子进程中执行外部命令时,我们使用了`execvp`,它会根据环境变量PATH来查找可执行文件。 另外,我们假设用户输入的命令参数不超过`MAX_ARGS`个。 最后,关于`stop`命令,我们只能终止最近启动的后台进程。如果有多个后台进程,用户只能终止最近的一个,无法终止更早的。如果需要终止任意后台进程,我们需要记录所有后台进程的PID,并让`stop`命令带参数(指定PID),但题目没有要求,所以简化处理。 以上代码是一个框架,可能需要根据实际编译环境进行调整。</think>下面我将为您详细解释如何编写一个支持基本功能的简易 Shell 程序,并提供完整代码实现。 ### 简易 Shell 程序实现方案 #### 核心设计思路 1. **主循环结构**:持续接收用户输入 2. **命令解析**:分割输入为命令和参数 3. **进程管理**: - 外部命令:通过 `fork()` + `execvp()` 在子进程执行 - 内建命令:直接在当前进程处理 4. **特殊命令处理**: - `stop`:终止后台进程 - `exit`:退出 Shell ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> #define MAX_INPUT 1024 #define MAX_ARGS 64 // 全局变量记录后台进程PID pid_t background_pid = -1; void execute_command(char **args, int background) { pid_t pid = fork(); if (pid == 0) { // 子进程 execvp(args[0], args); perror("execvp failed"); exit(EXIT_FAILURE); } else if (pid > 0) { // 父进程 if (!background) { waitpid(pid, NULL, 0); // 等待前台进程结束 } else { background_pid = pid; // 记录后台进程PID printf("[%d] running in background\n", pid); } } else { perror("fork failed"); } } int main() { char input[MAX_INPUT]; char *args[MAX_ARGS]; char *token; int background; while (1) { printf("mysh> "); fflush(stdout); // 读取用户输入 if (!fgets(input, MAX_INPUT, stdin)) break; // 处理换行符 input[strcspn(input, "\n")] = '\0'; // 解析输入 int i = 0; token = strtok(input, " "); while (token != NULL && i < MAX_ARGS - 1) { args[i++] = token; token = strtok(NULL, " "); } args[i] = NULL; // 空命令处理 if (i == 0) continue; // 检查后台运行标识 (&) background = 0; if (i > 0 && strcmp(args[i-1], "&") == 0) { background = 1; args[i-1] = NULL; } // 内建命令处理 if (strcmp(args[0], "exit") == 0) { break; } else if (strcmp(args[0], "stop") == 0) { if (background_pid != -1) { kill(background_pid, SIGTERM); printf("Terminated process [%d]\n", background_pid); background_pid = -1; } else { printf("No background process running\n"); } } else if (strcmp(args[0], "cd") == 0) { // 内建cd命令 if (args[1] && chdir(args[1]) != 0) { perror("cd failed"); } } else { // 执行外部命令 execute_command(args, background); } } return EXIT_SUCCESS; } ``` #### 关键功能实现说明 1. **命令执行流程**: - 使用 `fork()` 创建子进程 - 子进程调用 `execvp()` 执行外部命令 - 父进程使用 `waitpid()` 等待前台命令完成 2. **后台进程管理**: - 命令末尾添加 `&` 标识后台运行 - 后台进程 PID 存储在全局变量中 - `stop` 命令使用 `kill()` 发送 SIGTERM 信号终止进程 3. **内建命令处理**: - `cd`:直接调用 `chdir()` 改变工作目录[^2] - `exit`:退出主循环 - `stop`:终止后台进程 4. **输入解析**: - 使用 `strtok()` 分割命令行参数 - 支持带空格的长参数 - 自动处理末尾的换行符 #### 使用示例 ```bash # 编译程序 gcc -o mysh mysh.c # 运行Shell ./mysh # 示例命令 mysh> ls -l # 执行ls命令 mysh> sleep 30 & # 后台运行sleep [1234] running in background mysh> stop # 终止后台进程 Terminated process [1234] mysh> cd /tmp # 切换目录 mysh> exit # 退出Shell ``` ### 功能扩展建议 1. **信号处理**:添加 SIGINT (Ctrl+C) 处理 2. **进程状态跟踪**:实现 `jobs` 命令查看后台进程 3. **输入重定向**:支持 `<` 和 `>` 操作符 4. **管道功能**:实现命令间管道通信 5. **命令历史**:记录和调用历史命令
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值