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

news2024/10/5 10:12:38
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) 是为了确保数据立刻被写入文件,而不是缓存在内存中,以便实时分析。

实现文件系统监控

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189703.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NIM简单实践-图像分割

项目背景 我正在学习一个图像分割的 Demo&#xff0c;使用 NVIDIA 提供的预训练大模型进行光学字符检测 (OCDNet) 和光学字符识别 (OCRNet)。这些模型专门为光学字符检测和识别设计&#xff0c;能够自动将图像中的字符进行分割和识别。 预训练模型介绍 OCDNet (Optical Char…

Windows NTLM中继攻击(PortBender二进制可执行文件)

Windows NTLM中继攻击&#xff08;PortBender二进制可执行文件) 前言 最近在完善自己的一套TTPs&#xff08;战术、技术和程序&#xff09;以应对未来的网络作战、项目和攻防演练需求&#xff0c;翻到了PortBender&#xff0c;我觉得不依赖C2和影响主机本身实现这一切非常有趣…

如何使用ssm实现民族大学创新学分管理系统分析与设计+vue

TOC ssm763民族大学创新学分管理系统分析与设计vue 第1章 绪论 1.1 课题背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上不…

Linux 生产者消费者模型

前言 生产者消费者模型&#xff08;CP模型&#xff09;是一种十分经典的设计&#xff0c;常常用于多执行流的并发问题中&#xff01;很多书上都说他很高效&#xff0c;但高效体现在哪里并没有说明&#xff01;本博客将详解&#xff01; 目录 前言 一、生产者消费者模型 1.…

绝美的登录界面!滑动切换效果

绝美登录界面&#xff01;添加了管理员账号和测试账号 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><scri…

RC正弦波振荡电路

0、判断电路能否产生正弦波震荡的条件 如上图所示&#xff0c; Xo:输出量&#xff1b; A:放大器的增益&#xff1b; F:反馈系数。 上式分别为RC正弦波震荡器的幅值条件和相位条件&#xff0c;为了使输出量在合闸后能够有一个从小到大直至平衡在一定幅值的过程&#xff0c;电…

《Linux服务与安全管理》| 配置YUM源并验证

《Linux服务与安全管理》配置YUM源并验证 目录 《Linux服务与安全管理》配置YUM源并验证 任务一&#xff1a;配置本地YUM源 任务二&#xff1a;配置网络YUM源 学生姓名 **** 学号 **** 专业 **** 任务名称 配置YUM源并验证 完成日期 **** 任务目标 知识 了解配…

docker安装kafka-manager

kafkamanager docker安装_mob64ca12d80f3a的技术博客_51CTO博客 # 1、拉取镜像及创建容器 docker pull hlebalbau/kafka-manager docker run -d --name kafka-manager -p 9000:9000 --networkhost hlebalbau/kafka-manager# 2、增设端口 腾讯云# 3、修改防火墙 sudo firewall-…

Salesforce AI 推全新大语言模型评估家族SFR-Judge 基于Llama3构建

在自然语言处理领域&#xff0c;大型语言模型&#xff08;LLMs&#xff09;的发展迅速&#xff0c;已经在多个领域取得了显著的进展。不过&#xff0c;随着模型的复杂性增加&#xff0c;如何准确评估它们的输出就变得至关重要。传统上&#xff0c;我们依赖人类来进行评估&#…

【目标检测】yolo的三种数据集格式

目标检测中数据集格式之间的相互转换--coco、voc、yolohttps://zhuanlan.zhihu.com/p/461488682?utm_mediumsocial&utm_psn1825483604463071232&utm_sourcewechat_session【目标检测】yolo的三种数据集格式https://zhuanlan.zhihu.com/p/525950939?utm_mediumsocial&…

Python小示例——质地不均匀的硬币概率统计

在概率论和统计学中&#xff0c;随机事件的行为可以通过大量实验来研究。在日常生活中&#xff0c;我们经常用硬币进行抽样&#xff0c;比如抛硬币来决定某个结果。然而&#xff0c;当我们处理的是“质地不均匀”的硬币时&#xff0c;事情就变得复杂了。质地不均匀的硬币意味着…

【宽搜】4. leetcode 103 二叉树的锯齿形层序遍历

1 题目描述 题目链接&#xff1a;二叉树的锯齿形层序遍历 2 题目解析 根据题目描述&#xff0c;第一行是从左往右遍历&#xff0c;第二行是从右往左遍历。和层序遍历的区别就是&#xff1a; 在偶数行需要从右往左遍历。 因此&#xff0c;只需要在层序遍历的基础上增加一个变…

网络基础:TCP/IP五层模型、数据在局域网传输和跨网络传输的基本流程、IP地址与MAC地址的简单解析

目录 背景介绍 网络协议 OSI七层模型 TCP/IP五层模型 TCP/IP协议与OS的关系 网络协议的本质 数据在局域网传输的基本流程 MAC地址 报文的封装和解包 补充内容 数据的跨网络传输基本流程 IP地址 IP地址和MAC地址的区别 ​​​ 背景介绍 网络的发展经理了四个阶段…

dijstra算法——单元最短路径算法

Dijkstra算法 用来计算从一个点到其他所有点的最短路径的算法&#xff0c;是一种单源最短路径算法。也就是说&#xff0c;只能计算起点只有一个的情况。Dijkstra的时间复杂度是O(n^2)&#xff0c;它不能处理存在负边权的情况。 算法描述&#xff1a; 设起点为s&#xff0c;d…

云原生(四十六) | MySQL软件安装部署

文章目录 MySQL软件安装部署 一、MySQL软件部署步骤 二、安装MySQL MySQL软件安装部署 一、MySQL软件部署步骤 第一步&#xff1a;删除系统自带的mariadb 第二步&#xff1a;下载MySQL源&#xff0c;安装MySQL软件 第三步&#xff1a;启动MySQL&#xff0c;获取默认密码…

【无标题】提升快递管理效率的必备技能:教你批量查询与导出物流信息

在当今快节奏的商业环境中&#xff0c;快递与物流行业的效率直接关系到企业的运营成本和客户满意度。随着订单量的不断增加&#xff0c;如何高效地管理和追踪大量的物流信息成为了企业面临的一大挑战。批量查询与导出物流信息作为一种高效的数据处理手段&#xff0c;正逐渐成为…

信息安全工程师(33)访问控制概述

前言 访问控制是信息安全领域中至关重要的一个环节&#xff0c;它提供了一套方法&#xff0c;旨在限制用户对某些信息项或资源的访问权限&#xff0c;从而保护系统和数据的安全。 一、定义与目的 定义&#xff1a;访问控制是给出一套方法&#xff0c;将系统中的所有功能和数据…

ElliQ 老年身边的陪伴

前记 国庆回家发现爸爸之前干活脚崴了&#xff0c;找个临时拐杖撑住&#xff0c;我心里很不是滋味。虽然总和爸妈说&#xff0c;不要干重活&#xff0c;但老人总是担心成为儿女的负担&#xff0c;所以只要能动&#xff0c;就找活干。 给爸妈一点零花钱&#xff0c;老妈只收了…

多系统萎缩患者的运动指南【健康守护,动出希望】

亲爱的朋友们&#xff0c;今天我们来聊聊一个特别而重要的话题——多系统萎缩患者的运动指南。面对这一挑战&#xff0c;适量的运动不仅能缓解病情&#xff0c;还能提升生活质量。让我们一起&#xff0c;用爱与坚持&#xff0c;为生命加油&#xff01; &#x1f308; ‌为什么…

Linux系统字符命令关机方法对比

一、相同点&#xff1a;都可以达到关机或重启系统的目的。 二、不同点&#xff1a;命令内部的工作过程不同。 1、shutdown 安全的关机命令&#xff1a;系统管理员会通知所有登录的用户系统将要关闭且 login 指令会被冻结&#xff0c;即新的用户不能再登录。根据使用的参数不同…