1.3 实践与案例分析
1.3.1 案例分析:实现一个简单的Shell
本节将通过一个简单的Shell程序来展示如何使用C语言中的高级操作系统功能,包括命令行解析、进程管理(fork
和exec
)、管道和重定向。
1.3.1.1 解析命令行输入
在实现Shell时,第一步是解析用户输入的命令行。这一过程包括读取输入、分割命令和参数。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define MAX_INPUT_SIZE 1024
#define MAX_ARG_SIZE 100// 函数 parse_input:解析用户输入
void parse_input(char *input, char **args) {char *token;token = strtok(input, " \n"); // 使用空格和换行符作为分隔符 [1]int i = 0;while (token != NULL) {args[i++] = token; // 将分割的片段存入 args 数组 [2]token = strtok(NULL, " \n"); // 获取下一个分割片段 [3]}args[i] = NULL; // 参数列表以 NULL 结尾 [4]
}int main() {char input[MAX_INPUT_SIZE];char *args[MAX_ARG_SIZE];while (1) {printf("my_shell> ");if (fgets(input, MAX_INPUT_SIZE, stdin) == NULL) { // 从标准输入读取一行 [5]perror("fgets error"); // 错误处理 [6]exit(1);}parse_input(input, args); // 解析输入 [7]for (int i = 0; args[i] != NULL; i++) { // 输出解析完成后的参数 [8]printf("Argument %d: %s\n", i, args[i]); }}return 0;
}
- [1] 使用空格和换行符作为分隔符:
strtok
函数用于将input
中的字符串分割成若干个部分,依据空格和换行符进行分割。 - [2] 将分割的片段存入
args
数组:分割后的每个子字符串被存放在args
数组中,args
作为指针数组,每个元素指向一个字符串片段。 - [3] 获取下一个分割片段:通过在
strtok
函数中传入NULL
,继续获取下一个分割的字符串片段,直到没有更多的分割片段可获取。 - [4] 参数列表以 NULL 结尾:为了便于后续遍历,通过在
args
数组的末尾加上NULL
来标记参数列表的结束。 - [5] 从标准输入读取一行:
fgets
用于从标准输入流(stdin
)中读取一行输入,并存储到input
数组中。 - [6] 错误处理:若
fgets
返回NULL
,则表示出现错误,使用perror
打印错误信息并退出程序。 - [7] 解析输入:调用
parse_input
函数,将用户输入解析成命令和参数。 - [8] 输出解析完成后的参数:循环遍历
args
数组,输出每个解析出来的命令或参数。
1.3.1.2 使用fork
和exec
执行命令
在Shell中,用户命令通过创建子进程并使用exec
族函数执行。
示例代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>// 执行命令函数
void execute_command(char **args) {pid_t pid = fork(); // 创建子进程 [1]if (pid == 0) { // 子进程逻辑if (execvp(args[0], args) == -1) { // 执行命令 [2]perror("exec error"); // 输出执行错误信息exit(1); // 执行失败时退出}} else if (pid < 0) { // fork 创建失败 [3]perror("fork error"); // 输出错误信息} else { // 父进程逻辑wait(NULL); // 等待子进程结束 [4]}
}int main() {char input[MAX_INPUT_SIZE];char *args[MAX_ARG_SIZE];while (1) {printf("my_shell> "); // 输出提示符if (fgets(input, MAX_INPUT_SIZE, stdin) == NULL) { // 获取输入 [5]perror("fgets error"); // 输出读取错误信息exit(1);}parse_input(input, args); // 解析用户输入 [6]if (args[0] == NULL) continue; // 处理空输入 [7]execute_command(args); // 执行命令 [8]}return 0;
}
- [1] 创建子进程:
fork()
负责创建一个新的进程,通过返回值区分父进程和子进程。在父进程中,fork()
返回子进程的 PID;在子进程中,返回0;若失败,返回-1。 - [2] 执行命令:
execvp()
用于在子进程中运行用户指定的命令。它接收命令和参数数组,如果执行失败,返回 -1。 - [3] fork失败:发生错误时,
fork()
返回负值,并输出错误信息。 - [4] 等待子进程结束:
wait(NULL)
是一种阻塞调用,它使父进程等待子进程的完成以确保操作的有序性。 - [5] 获取输入:
fgets()
从标准输入读取用户命令,并存储在input
数组中。 - [6] 解析用户输入:
parse_input()
是一个假定已存在的函数,用于将输入的字符串解析为命令和参数。这一函数的具体实现未在示例中提供,需要自行实现。 - [7] 处理空输入:在解析结果
args
的第一个元素为NULL
时表示无效输入,程序继续等待下一次输入。 - [8] 执行命令:通过调用
execute_command()
来实际执行用户输入的命令。
1.3.1.3 管道与重定向的实现
管道和重定向是Shell功能中的重要组成部分。通过管道,可以将一个命令的输出作为下一个命令的输入,而重定向可以将命令的输出重定向到文件中或从文件读取输入。
管道示例代码
#include <unistd.h> // 包含POSIX API,用于管道、进程控制
#include <sys/types.h> // 定义数据类型,包括`pid_t`
#include <sys/wait.h> // 用于进程等待
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 标准库函数,包含`exit()`// 函数 execute_pipe:通过管道连接两个命令
void execute_pipe(char **args1, char **args2) {int pipefd[2]; // 用于保存管道的文件描述符 [1]pid_t pid1, pid2;if (pipe(pipefd) == -1) { // 创建管道perror("pipe error");exit(1);}pid1 = fork();if (pid1 == 0) { // 第一个子进程close(pipefd[0]); // 关闭管道读端 [2]dup2(pipefd[1], STDOUT_FILENO); // 将标准输出重定向到管道写端 [3]close(pipefd[1]); // 关闭不再需要的写端if (execvp(args1[0], args1) == -1) { // 执行第一个命令perror("exec error");exit(1);}}pid2 = fork();if (pid2 == 0) { // 第二个子进程close(pipefd[1]); // 关闭管道写端 [4]dup2(pipefd[0], STDIN_FILENO); // 将标准输入重定向到管道读端 [5]close(pipefd[0]); // 关闭不再需要的读端if (execvp(args2[0], args2) == -1) { // 执行第二个命令perror("exec error");exit(1);}}close(pipefd[0]); // 父进程:关闭管道读端 [6]close(pipefd[1]); // 父进程:关闭管道写端 [7]wait(NULL); // 等待第一个子进程完成 [8]wait(NULL); // 等待第二个子进程完成 [9]
}int main() {// 示例:将 `ls` 结果通过管道传递给 `wc -l`char *args1[] = {"ls", NULL}; // 第一个命令参数 [10]char *args2[] = {"wc", "-l", NULL}; // 第二个命令参数 [11]execute_pipe(args1, args2);return 0;
}
- [1] 管道文件描述符:
pipefd[2]
创建一个管道,通过数组保存读写端的文件描述符,其中pipefd[0]
用于读,pipefd[1]
用于写。 - [2] 关闭读端:在第一个子进程中,我们只需要写端,用于接收命令的输出,因此关闭读端。
- [3] 重定向输出:
dup2(pipefd[1], STDOUT_FILENO)
使得标准输出(文件描述符1)指向管道的写端。 - [4] 关闭写端:在第二个子进程中,我们只需要读端,用于接收另一个命令的输入,因此关闭写端。
- [5] 重定向输入:
dup2(pipefd[0], STDIN_FILENO)
使标准输入(文件描述符0)指向管道的读端。 - [6][7] 父进程关闭管道:父进程应当关闭所有的管道描述符以避免资源浪费。
- [8][9] 等待子进程:父进程使用
wait(NULL)
函数等待子进程完成,以防止僵尸进程的产生。 - [10][11] 命令参数:
char *args1[]
和char *args2[]
定义了要执行的命令及其参数,用于execvp()
函数。
此代码演示了基本的进程间通信机制,有助于构建功能更复杂的Shell程序,包括实现命令的解析、执行以及其他类型的输入输出重定向等。
1.3.2 案例分析:实现一个文件系统监控工具
在这部分,我们将讨论如何使用inotify
系统调用来监听文件系统事件,并将检测到的事件记录到日志中。inotify
是Linux内核提供的一个功能,能够监控文件系统的诸如创建、删除、修改等事件。使用inotify
可以帮助我们实现高效的文件系统监控工具。
1.3.2.1 使用系统调用监听文件事件(inotify
)
inotify
是一种强大的机制,可以用来监控文件或目录的变化。以下是使用inotify
的基本步骤:
-
初始化
inotify
实例:通过系统调用inotify_init
或inotify_init1
来创建一个新的inotify
实例,并返回一个文件描述符。#include <sys/inotify.h> #include <stdio.h> #include <stdlib.h>int inotify_fd = inotify_init(); // 初始化 inotify 实例 [1] if (inotify_fd < 0) {perror("inotify_init"); // 错误处理 [2]exit(EXIT_FAILURE); // 程序退出 [3] }
-
[1] 初始化 inotify 实例:
inotify_fd = inotify_init();
调用inotify_init()
函数初始化一个 inotify 实例。inotify
是 Linux 内核提供的一个功能,用于监控文件系统事件,比如文件的创建、删除、修改等。返回的文件描述符inotify_fd
用于后续对文件系统事件的监控操作。- 知识点:
inotify_init()
返回一个文件描述符,它被用于标识 inotify 实例。- 如果
inotify_init()
返回-1,则表示初始化失败。
- 知识点:
-
[2] 错误处理:
perror("inotify_init");
用于处理inotify_init()
调用失败的情况,打印错误信息 tostderr
。perror()
函数会输出自定义的错误提示信息和上一个函数调用导致的错误信息(通过检查errno
)。- 知识点:
- 检查函数返回值对于检测和处理错误很重要。
perror()
是一种处理错误并向用户提供错误源信息的标准方法。
- 知识点:
-
[3] 程序退出:
exit(EXIT_FAILURE);
在inotify_init()
失败后确保程序安全退出。EXIT_FAILURE
是标准库 cstdlib 中定义的错误退出状态码,通常表示程序异常结束。- 知识点:
- 通过
exit()
终止程序并可以返回一个状态码给调用环境。 EXIT_FAILURE
和EXIT_SUCCESS
是标准宏定义,用于表示程序的退出状态,通常用作返回值来指示程序是否正常终止或是遇到错误。
- 通过
- 知识点:
-
-
添加需要监控的文件或目录:使用
inotify_add_watch
将指定的文件或目录添加到inotify
实例中,并指定需要监控的事件。
int wd = inotify_add_watch(inotify_fd, "/path/to/watch", IN_CREATE | IN_DELETE | IN_MODIFY); // 添加监视器 [1]
if (wd < 0) {perror("inotify_add_watch"); // 错误输出 [2]exit(EXIT_FAILURE); // 退出程序 [3]
}
- [1] 添加监视器:将路径
"/path/to/watch"
添加到inotify
的监视列表,并指定感兴趣的事件(IN_CREATE
、IN_DELETE
、IN_MODIFY
)。 - [2] 错误输出:通过
perror
函数输出错误信息,当监视器添加失败时,此函数根据errno
的值输出详细的错误描述。 - [3] 退出程序:调用
exit(EXIT_FAILURE)
以非零状态退出程序,表示因错误而终止。正常退出状态为零,非零值通常用来表示错误状态。
-
读取
inotify
事件:使用read
系统调用,从文件描述符中读取事件。这些事件将会存在一个特定的缓冲区中。char buffer[1024] __attribute__ ((aligned(__alignof__(struct inotify_event)))); // [1] const struct inotify_event *event; // [2] ssize_t len;while (1) {len = read(inotify_fd, buffer, sizeof(buffer)); // [3]if (len < 0) {perror("read");exit(EXIT_FAILURE);}for (char *ptr = buffer; ptr < buffer + len; ptr += sizeof(struct inotify_event) + event->len) { // [4]event = (const struct inotify_event *) ptr; // [5]if (event->len) {printf("File %s was %s\n", event->name, (event->mask & IN_CREATE) ? "created" : // [6](event->mask & IN_DELETE) ? "deleted" : "modified");}} }
-
[1] 缓冲区对齐和 attribute:
char buffer[1024] __attribute__ ((aligned(__alignof__(struct inotify_event))));
这里使用了__attribute__
指令来确保buffer
按照struct inotify_event
的对齐方式对齐。这是为了满足特定架构对内存对齐的要求,从而提高性能或避免错误。 -
[2] inotify_event 结构体指针:
const struct inotify_event *event;
声明了一个指针,用于指向在buffer
中读取的事件。 -
[3] 读取事件信息:
read(inotify_fd, buffer, sizeof(buffer));
通过read
函数从 inotify 文件描述符inotify_fd
中读取事件数据,存储在buffer
中。 -
[4] 遍历所有事件:通过一个循环遍历
buffer
中的所有事件。ptr
从buffer
开始,步进大小是每个struct inotify_event
的大小加上event->len
(即事件名的长度),逐个读取事件并处理。 -
[5] 事件指针调整:
event = (const struct inotify_event *) ptr;
将当前指针ptr
所指的数据区段转换为struct inotify_event
结构体进行处理。 -
[6] 事件检测与输出:
printf("File %s was %s\n", event->name, ...);
通过检查event->mask
中的标志位,决定文件是被创建、删除还是修改,并打印对应信息。IN_CREATE
、IN_DELETE
、IN_MODIFY
是 inotify 事件中常用的掩码宏定义用于表示不同的文件系统事件。
-
1.3.2.2 日志记录与分析
为了更好地跟踪和分析文件系统变化,我们可以将这些事件记录到日志文件中。以下是一个简单的日志记录实现:
FILE *log_file = fopen("file_system_monitor.log", "a");
if (log_file == NULL) {perror("fopen");exit(EXIT_FAILURE);
}while (1) {len = read(inotify_fd, buffer, sizeof(buffer));if (len < 0) {perror("read");exit(EXIT_FAILURE);}for (char *ptr = buffer; ptr < buffer + len; ptr += sizeof(struct inotify_event) + event->len) {event = (const struct inotify_event *) ptr;if (event->len) {char *event_type = (event->mask & IN_CREATE) ? "created" : (event->mask & IN_DELETE) ? "deleted" : "modified";fprintf(log_file, "File '%s' was %s\n", event->name, event_type);fflush(log_file);}}
}
-
[1] 初始化
inotify
实例:首先需要使用inotify_init()
函数创建一个新的inotify
实例,该实例返回一个文件描述符inotify_fd
,用于监听文件系统事件。 -
[2] 添加监控:通过
inotify_add_watch()
函数,将需要监控的目录或文件加入到inotify
实例中,同时指定要监听的事件类型,比如文件的创建(IN_CREATE
)、删除(IN_DELETE
)、修改(IN_MODIFY
)等事件。一旦这些事件在指定目录或文件上发生,inotify
将报告这些事件。 -
[3] 读取事件:使用
read()
系统调用从inotify_fd
读取已经触发的事件。读取的数据存储在缓冲区中,其中一个事件数据包含多个struct inotify_event
结构,需逐个分析。 -
[4] 事件日志记录:对于每一个获取到的
inotify_event
事件,检查其事件类型(新建、删除、修改),然后使用fprintf()
将事件写入日志文件中file_system_monitor.log
。执行fflush(log_file)
是为了确保数据立刻被写入文件,而不是缓存在内存中,以便实时分析。
实现文件系统监控
通过这样的实现,您可以构建一个简单但有效的文件系统监控工具,实时监听文件或目录的变化,并记录这些事件至日志文件,以便后续进行详细的分析。这种机制对于检测未经授权的文件访问、审计文件操作活动以及其它与文件系统有关的监控应用非常有用。