自学内容网 自学内容网

【C语言系统编程】【第一部分:操作系统知识】1.3.实践与案例分析

1.3 实践与案例分析
1.3.1 案例分析:实现一个简单的Shell

本节将通过一个简单的Shell程序来展示如何使用C语言中的高级操作系统功能,包括命令行解析、进程管理(forkexec)、管道和重定向。

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 使用forkexec执行命令

在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的基本步骤:

  1. 初始化inotify实例:通过系统调用inotify_initinotify_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() 调用失败的情况,打印错误信息 to stderrperror() 函数会输出自定义的错误提示信息和上一个函数调用导致的错误信息(通过检查 errno)。

      • 知识点
        • 检查函数返回值对于检测和处理错误很重要。
        • perror() 是一种处理错误并向用户提供错误源信息的标准方法。
    • [3] 程序退出exit(EXIT_FAILURE);inotify_init() 失败后确保程序安全退出。EXIT_FAILURE 是标准库 cstdlib 中定义的错误退出状态码,通常表示程序异常结束。

      • 知识点
        • 通过 exit() 终止程序并可以返回一个状态码给调用环境。
        • EXIT_FAILUREEXIT_SUCCESS 是标准宏定义,用于表示程序的退出状态,通常用作返回值来指示程序是否正常终止或是遇到错误。
  2. 添加需要监控的文件或目录:使用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_CREATEIN_DELETEIN_MODIFY)。
  • [2] 错误输出:通过 perror 函数输出错误信息,当监视器添加失败时,此函数根据 errno 的值输出详细的错误描述。
  • [3] 退出程序:调用 exit(EXIT_FAILURE) 以非零状态退出程序,表示因错误而终止。正常退出状态为零,非零值通常用来表示错误状态。
  1. 读取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] 缓冲区对齐和 attributechar 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 中的所有事件。ptrbuffer 开始,步进大小是每个 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_CREATEIN_DELETEIN_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) 是为了确保数据立刻被写入文件,而不是缓存在内存中,以便实时分析。

实现文件系统监控

通过这样的实现,您可以构建一个简单但有效的文件系统监控工具,实时监听文件或目录的变化,并记录这些事件至日志文件,以便后续进行详细的分析。这种机制对于检测未经授权的文件访问、审计文件操作活动以及其它与文件系统有关的监控应用非常有用。


原文地址:https://blog.csdn.net/fjw12998/article/details/142710741

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!