C语言如何实现killall命令功能?

99ANYc3cd6
预计阅读时长 23 分钟
位置: 首页 C语言 正文

killall 命令的核心功能是:根据进程的名称,找到所有匹配的进程 ID (PID),然后向它们发送一个指定的信号(默认是 SIGTERM,即终止信号)。

c语言 killall
(图片来源网络,侵删)

在 C 语言中,我们可以通过调用 Linux/Unix 系统提供的 API 来实现这个功能,主要会用到以下几个头文件和函数:

  1. <dirent.h>: 用于读取目录,特别是 /proc 目录,这里是存放所有进程信息的地方。
  2. <unistd.h>: 提供 getpid() 等函数。
  3. <sys/types.h>: 定义数据类型,如 pid_t
  4. <signal.h>: 定义信号相关的常量,如 SIGTERM, SIGKILL
  5. <stdlib.h>: 提供 atoi() 函数,用于将字符串转换为整数。
  6. <string.h>: 提供 strcmp() 函数,用于字符串比较。
  7. <stdio.h>: 用于标准输入输出,如 printf

实现思路

  1. 打开 /proc 目录/proc 是一个虚拟文件系统,它包含了正在运行的进程的所有信息,每个进程都有一个以其 PID 命名的子目录。
  2. 遍历 /proc 目录:读取 /proc 下的每一个条目。
  3. 识别进程目录:尝试将条目名转换为数字(PID),如果转换成功,则认为它是一个有效的进程目录。
  4. 读取进程的命令名:进入每个进程目录(/proc/1234/),读取 commcmdline 文件的内容。
    • /proc/[pid]/comm: 包含进程的基本名称。
    • /proc/[pid]/cmdline: 包含进程启动时执行的完整命令行,第一个 \0 之前的部分通常是可执行文件的路径或名称,我们通常使用 basename 来获取这个路径的最后一部分,以便与用户提供的进程名进行比较。
  5. 比较进程名:将读取到的进程名与用户指定的进程名进行比较。
  6. 发送信号:如果匹配成功,则使用 kill() 函数向该 PID 发送指定的信号。
  7. 处理错误:在整个过程中,需要妥善处理文件打开、读取失败等错误情况。

C 语言代码实现

下面是一个完整的 C 语言程序,它模仿了 killall 的基本功能。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <errno.h>
// 函数声明
void kill_process_by_name(const char *process_name, int signal);
int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <process_name> [signal]\n", argv[0]);
        fprintf(stderr, "Example: %s firefox\n", argv[0]);
        fprintf(stderr, "Example: %s nginx -9\n", argv[0]);
        return 1;
    }
    const char *process_name = argv[1];
    int signal_to_send = SIGTERM; // 默认信号为 SIGTERM (15)
    // 如果提供了第三个参数,则将其作为信号
    if (argc > 2) {
        signal_to_send = atoi(argv[2]);
        if (signal_to_send <= 0) {
            fprintf(stderr, "Error: Invalid signal number '%s'. Using SIGTERM.\n", argv[2]);
            signal_to_send = SIGTERM;
        }
    }
    printf("Attempting to kill processes with name '%s' using signal %d...\n", process_name, signal_to_send);
    kill_process_by_name(process_name, signal_to_send);
    return 0;
}
/**
 * @brief 根据进程名向所有匹配的进程发送指定信号
 * @param process_name 要查找的进程名
 * @param signal_to_send 要发送的信号
 */
void kill_process_by_name(const char *process_name, int signal_to_send) {
    DIR *dir;
    struct dirent *entry;
    // 打开 /proc 目录
    dir = opendir("/proc");
    if (dir == NULL) {
        perror("Error opening /proc");
        return;
    }
    // 遍历 /proc 目录下的每一个条目
    while ((entry = readdir(dir)) != NULL) {
        // 跳过 '.' 和 '..' 以及非数字目录
        if (entry->d_type != DT_DIR || strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            // 使用 atoi 尝试转换,如果转换失败则不是 PID 目录
            if (atoi(entry->d_name) == 0) {
                continue;
            }
        }
        pid_t pid = atoi(entry->d_name);
        char comm_path[256];
        char cmdline_path[256];
        FILE *fp;
        char buffer[256];
        char *cmd_name = NULL;
        // 构建 /proc/[pid]/comm 文件路径
        snprintf(comm_path, sizeof(comm_path), "/proc/%d/comm", pid);
        // 构建 /proc/[pid]/cmdline 文件路径
        snprintf(cmdline_path, sizeof(cmdline_path), "/proc/%d/cmdline", pid);
        // 优先尝试从 cmdline 获取进程名,因为它更准确
        fp = fopen(cmdline_path, "r");
        if (fp) {
            if (fgets(buffer, sizeof(buffer), fp) != NULL) {
                // cmdline 文件的内容以 '\0' 分隔,我们取第一个部分
                // 使用 basename 获取路径的最后一部分
                char *slash = strrchr(buffer, '/');
                if (slash) {
                    cmd_name = slash + 1;
                } else {
                    cmd_name = buffer;
                }
            }
            fclose(fp);
        }
        // cmdline 读取失败或为空,则尝试从 comm 读取
        if (cmd_name == NULL || strlen(cmd_name) == 0) {
            fp = fopen(comm_path, "r");
            if (fp) {
                if (fgets(buffer, sizeof(buffer), fp) != NULL) {
                    cmd_name = buffer;
                }
                fclose(fp);
            }
        }
        // 如果成功获取了进程名并且与目标名匹配
        if (cmd_name != NULL && strcmp(cmd_name, process_name) == 0) {
            printf("Found matching process: PID = %d, Name = %s\n", pid, cmd_name);
            // 发送信号
            if (kill(pid, signal_to_send) == 0) {
                printf("Successfully sent signal %d to PID %d.\n", signal_to_send, pid);
            } else {
                // perror 会打印 "kill: ..." 这样的错误信息
                perror("Failed to send signal");
            }
        }
    }
    closedir(dir);
}

如何编译和运行

  1. 保存代码:将上面的代码保存为 my_killall.c

  2. 编译:打开终端,使用 gcc 进行编译。

    c语言 killall
    (图片来源网络,侵删)
    gcc my_killall.c -o my_killall
  3. 运行

    • 启动一个或多个你想测试的进程,geditfirefox

    • 然后运行你的程序:

      # 终止名为 "gedit" 的所有进程(发送 SIGTERM,信号15)
      ./my_killall gedit
      # 强制杀死名为 "nginx" 的所有进程(发送 SIGKILL,信号9)
      ./my_killall nginx -9

代码解释

  • main 函数:处理命令行参数,解析出进程名和要发送的信号,然后调用核心函数 kill_process_by_name
  • kill_process_by_name 函数:这是核心逻辑。
    • opendir("/proc"):打开 /proc 目录。
    • readdir(dir):循环读取目录中的每一个文件/目录名。
    • atoi(entry->d_name):检查目录名是否是有效的数字(PID)。
    • snprintf:安全地构建 /proc/[pid]/comm/proc/[pid]/cmdline 的文件路径。
    • fopenfgets:打开并读取 cmdlinecomm 文件的内容。
    • strrchr(buffer, '/'):在 cmdline 的路径中查找最后一个 ,以获取可执行文件的名称(basename 的简单实现)。
    • strcmp(cmd_name, process_name):比较读取到的进程名和用户指定的进程名。
    • kill(pid, signal_to_send):如果匹配,则调用 kill 函数发送信号。
  • 错误处理:使用 perror 函数在 kill() 失败时打印有意义的错误信息("No such process")。

与真实 killall 的区别

这个 C 语言实现是一个简化版的 killall,它有以下局限性:

c语言 killall
(图片来源网络,侵删)
  1. 不处理僵尸进程killall 通常不会影响僵尸进程(状态为 'Z' 的进程),因为它们已经不运行了,我们的代码没有检查进程状态。
  2. 用户权限:我们的代码没有检查当前用户是否有权限杀死目标进程,真实的 killall 会因为权限不足而报错。
  3. 更精确的匹配:真实的 killall 可能会有更复杂的匹配规则(-i 交互式确认,-r 正则表达式匹配等)。
  4. 信号处理:我们的代码对信号的处理非常基础,真实的 killall 对无效信号有更健壮的处理。

尽管如此,这个程序很好地展示了如何使用 C 语言的系统调用来实现一个常见的 Linux 命令的核心功能,是学习 Linux 系统编程的绝佳范例。

-- 展开阅读全文 --
头像
化妆品织梦免费模板
« 上一篇 04-16
C语言fork炸弹如何防御?
下一篇 » 04-16

相关文章

取消
微信二维码
支付宝二维码

目录[+]