当前位置:   article > 正文

Linux使用C语言获取进程信息

Linux使用C语言获取进程信息

Linux使用C语言获取进程信息

Author: OnceDay Date: 2024年2月22日

漫漫长路,才刚刚开始…

全系列文章可查看专栏: Linux实践记录_Once_day的博客-CSDN博客

参考文档:

1. Linux进程概述
1.1 进程介绍

在计算机的世界里,Linux进程是一个非常基础而且关键的概念。它可以被理解为正在执行的一个程序的实例。每个进程都有自己独特的身份,我们称之为进程ID(PID),就像每个人都有自己的身份证号码一样。Linux操作系统是一种多任务操作系统,可以同时运行多个进程,就像一个杂技团队能同时上演多个节目一样。

现在,想象一下进程是厨房里的一个厨师,而计算机资源(如CPU、内存)则是厨房里的炉子、锅碗瓢盆。每个厨师都需要这些资源来完成他们的烹饪任务。Linux系统就像一个细心的餐厅经理,确保所有厨师轮流合理使用这些资源,并监督他们的工作进度。

首先,进程的创建通常通过fork()系统调用完成。这个调用会创建一个与当前进程几乎完全相同的副本,包括代码、数据和堆栈,但拥有新的进程标识符(PID)。这个新进程可以继续执行,或者通过exec()系列函数来加载新的程序。

进程有不同的状态,包括运行(正在使用CPU)、等待(等待资源可用)、停止(被挂起)和僵尸(已完成执行但等待父进程读取状态的进程)。就像厨师有时在炒菜,有时在等待食材,有时在休息,有时则在等待经理来检查他们做的菜。

Linux提供了很多管理进程的工具,如ps命令可以查看当前的进程,top命令可以实时监控进程的状态,而kill命令可以用来结束某个进程。这就好比厨房里有各种管理工具,可以查看厨师的工作列表,监控他们的工作状态,或者让某个厨师停止当前的工作。

进程间通信(IPC)是Linux系统中进程协作的重要机制。常见的IPC方式有管道(pipe)、命名管道(named pipe)、信号(signal)、共享内存(shared memory)、消息队列(message queue)和套接字(socket)。这些机制允许进程间传递数据和同步操作。

进程的同步主要通过信号量(semaphore)和互斥锁(mutex)来实现。信号量用于控制对共享资源的访问,而互斥锁则确保同一时间只有一个进程可以访问某个资源。

进程的终止可以通过多种方式,如正常退出、被其他进程杀死(通过发送信号)、或系统强制终止。进程结束后,其资源会被回收,但进程的退出状态(exit status)会被保存,这对于调试和错误处理非常重要。

在Linux中,进程管理还涉及到进程的优先级(nice value)和实时调度。优先级决定了进程获取CPU时间片的优先程度,而实时调度则允许进程以更高的优先级运行,通常用于需要快速响应的任务。

最后,Linux系统还支持进程的层级关系,也就是父子进程关系。当一个进程创建一个新进程时,原来的进程就是父进程,新创建的就是子进程。这有点儿像家族企业,父亲开了一家餐馆后,孩子长大也可能在旁边开一家小吃店。

了解Linux进程是掌握操作系统和进行系统编程的基础。虽然这是一门复杂的技术,但它对于运行和管理Linux系统至关重要。不同的进程就像社会中不同的个体,它们相互独立却又相云协作,共同构成了一个运作高效的系统。

1.2 介绍/proc目录信息

在Linux操作系统中,/proc目录是一个非常特殊和有趣的存在,它实际上并不是存储在硬盘上的真实文件系统,而是一个虚拟文件系统,通常被称为进程信息伪文件系统(Process Information Pseudo-File System)。/proc提供了一个窗口,通过它可以窥见内核中的世界,包括系统信息及正在运行的进程详情。

/proc目录中的文件和子目录大都以数字命名,这些数字对应着系统中的进程ID(PID)。每一个这样的目录里面,包含了与该PID相关的信息,例如进程的内存映射(/proc/<pid>/maps)、环境变量(/proc/<pid>/environ)、可执行文件的链接(/proc/<pid>/exe)等等。这些文件为系统管理员和程序员提供了一种简便的方式来监控进程和系统的内部状态。

除了进程相关信息,/proc目录还包含了许多全系统范围的信息。例如,/proc/meminfo文件包含了内存使用情况的详细信息,/proc/cpuinfo提供了CPU的相关信息,/proc/net目录包含了网络协议和配置的信息,等等。

值得一提的是,/proc目录下的文件大多是可读的文本文件,可以直接使用常用的文本工具查看,如catless等。但也有一些文件是可写的,通过对这些文件写入特定的值,用户或程序员可以调整或配置内核参数,这是实现内核动态调优的一种方式。

一个常见的应用场景是,当系统管理员想要快速检查系统的运行状态,或者程序员在开发过程中需要获取系统或进程的某些信息时,他们就会查阅/proc目录下的文件。例如,通过读取/proc/uptime文件,可以获取系统已经运行了多长时间;通过查看/proc/loadavg文件,可以了解系统的平均负载情况。

以下是/proc目录下的一些常见子目录和文件的详细总结表格。请注意,这个表格并不包含所有可能存在的文件和目录,因为/proc目录的内容可能会根据Linux内核的版本和系统配置有所不同

路径描述
/proc/[pid]每个进程都有一个对应的目录,包含该进程的详细信息
/proc/[pid]/attr安全属性,如SELinux上下文
/proc/[pid]/auxvELF解释器传递给进程的信息
/proc/[pid]/cmdline进程启动时的完整命令行
/proc/[pid]/comm进程的命令名称
/proc/[pid]/coredump_filter核心转储文件的过滤设置
/proc/[pid]/cpusetCPU亲和性设置
/proc/[pid]/cwd当前工作目录的符号链接
/proc/[pid]/environ环境变量列表
/proc/[pid]/exe可执行文件的符号链接
/proc/[pid]/fd打开文件的文件描述符
/proc/[pid]/fdinfo文件描述符信息
/proc/[pid]/limits资源限制
/proc/[pid]/maps内存映射信息
/proc/[pid]/mem进程内存页访问
/proc/[pid]/mountinfo挂载信息
/proc/[pid]/mounts当前进程挂载的文件系统列表
/proc/[pid]/mountstats挂载统计信息
/proc/[pid]/ns命名空间信息
/proc/[pid]/numa_mapsNUMA内存映射信息
/proc/[pid]/oom_adjOOM(内存不足)调整值
/proc/[pid]/oom_scoreOOM评分
/proc/[pid]/oom_score_adjOOM评分调整
/proc/[pid]/root根目录的符号链接
/proc/[pid]/smaps内存映射的详细内存使用情况
/proc/[pid]/stat进程状态信息
/proc/[pid]/statm内存使用状态信息
/proc/[pid]/status进程状态信息(可读格式)
/proc/[pid]/task包含进程中每个线程的信息
/proc/[pid]/thread-self当前线程的信息
/proc/self当前进程的信息链接
/proc/self/task当前进程的线程信息
/proc/apm高级电源管理(APM)信息
/proc/buddyinfo内存碎片信息
/proc/cmdline启动时传递给内核的参数
/proc/cpuinfoCPU信息
/proc/crypto加密算法信息
/proc/devices设备列表
/proc/diskstats磁盘I/O统计信息
/proc/dmaISA DMA通道信息
/proc/execdomains执行域信息
/proc/fb帧缓冲设备信息
/proc/filesystems支持的文件系统类型
/proc/interruptsIRQ中断信息
/proc/iomem物理设备内存映射信息
/proc/ioports输入输出端口范围信息
/proc/kallsyms内核符号信息
/proc/kcore物理内存映射(核心文件格式)
/proc/kmsg内核消息记录
/proc/loadavgCPU和I/O负载平均值
/proc/locks内核锁定的文件信息
/proc/mdstatRAID配置信息
/proc/meminfo内存使用信息
/proc/mounts系统挂载信息
/proc/modules加载的内核模块列表
/proc/partitions分区信息
/proc/pciPCI设备信息
/proc/slabinfoSlab缓存信息
/proc/statCPU活动统计信息
/proc/sysrq-trigger系统请求触发器
/proc/swaps交换空间使用情况
/proc/uptime系统运行时间
/proc/version内核版本信息
/proc/bus总线信息
/proc/driver驱动信息
/proc/fs文件系统信息
/proc/ideIDE设备信息
/proc/irq中断请求设备信息
/proc/net网络设备信息
/proc/scsiSCSI设备信息
/proc/ttyTTY设备信息
/proc/net/dev网络适配器及统计信息
/proc/vmstat虚拟内存统计信息
/proc/vmcore内核panic时的内存映像
/proc/diskstats磁盘信息
/proc/schedstat调度器统计信息
/proc/zoneinfo内存空间统计信息

这个表格提供了/proc目录下一些关键文件和目录的简要描述。由于/proc目录的内容非常丰富,这里只列出了一部分。在实际使用中,可以通过ls -l /proc命令来查看当前系统/proc目录下的完整内容。

1.3 常用进程相关命令

可参考文档: Linux之(16)程序管理-CSDN博客

在Linux系统中,有一系列强大的进程相关工具命令,它们是系统管理员和开发者日常工作中不可或缺的助手。以下是一些常用的进程工具及其简要介绍和示例:

  1. ps(Process Status):这是最基本的进程查看工具,用于列出系统中当前运行的进程。例如,命令ps aux会列出系统中所有进程的详细信息,其中a代表所有用户的进程,u代表用户以及其他详细信息,x代表没有控制终端的进程。

    ubuntu->~:$ ps aux
    USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root           1  0.0  0.1 167488 11228 ?        Ss   Jan14   2:04 /sbin/init
    root           2  0.0  0.0      0     0 ?        S    Jan14   0:01 [kthreadd]
    root           3  0.0  0.0      0     0 ?        I<   Jan14   0:00 [rcu_gp]
    ......
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  2. top:这个命令提供了一个实时更新的进程状态动态视图。它显示了CPU和内存的使用情况,以及进程的动态列表,可以实时查看系统的性能状况。只需在终端输入top即可启动。

    ubuntu->~:$ top
    top - 22:54:50 up 38 days, 23:36,  1 user,  load average: 0.06, 0.05, 0.01
    Tasks: 123 total,   1 running, 122 sleeping,   0 stopped,   0 zombie
    %Cpu(s):  0.0 us,  1.6 sy,  0.0 ni, 98.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
    MiB Mem :   7437.5 total,   2603.7 free,    400.8 used,   4433.0 buff/cache
    MiB Swap:      0.0 total,      0.0 free,      0.0 used.   6719.4 avail Mem 
    
        PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                             
          1 root      20   0  167488  11228   6444 S   0.0   0.1   2:04.38 systemd                             
          2 root      20   0       0      0      0 S   0.0   0.0   0:01.59 kthreadd                            
          3 root       0 -20       0      0      0 I   0.0   0.0   0:00.00 rcu_gp                              
    ......   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  3. htop:相比tophtop提供了一个更为友好和可交互的界面,它允许用户通过键盘操作进行进程管理,如结束进程等。启动方法是在终端输入htop

在这里插入图片描述

  1. kill:这是用于发送信号给进程的命令,通常用于终止进程。例如,kill -9 <pid>会发送SIGKILL信号来强制终止指定PID的进程。

  2. pkillkillall:这两个命令也用于发送信号给进程,但它们可以根据进程名而不是PID来指定进程。例如,pkill nginx将终止所有名为nginx的进程。

  3. pstree:该命令用于以树形结构显示进程的层次关系,这对于理解进程之间的父子关系非常有帮助。只需输入pstree即可显示当前进程树。

    ubuntu->~:$ pstree
    systemd─┬─ModemManager───2*[{ModemManager}]
            ├─YDLive─┬─YDService─┬─sh───10*[{sh}]
            │        │           └─23*[{YDService}]
            │        └─10*[{YDLive}]
            ├─acpid
            ├─2*[agetty]
            ├─auditd───{auditd}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  4. vmstat:虽然不是直接用于进程管理,但vmstat命令能提供关于系统内存、交换、IO、CPU活动等重要信息,这对于分析进程性能有重要的参考价值。例如,vmstat 1会每秒刷新显示系统状态。

    ubuntu->~:$ vmstat 
    procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
     r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
     0  0      0 2665612 220868 4320632    0    0    41    25    0    3  0  0 99  0  0
    
    • 1
    • 2
    • 3
    • 4
  5. lsof:List Open Files的缩写,用于列出被进程打开的文件,对于查找使用了哪些文件或设备非常有用。例如,lsof -p <pid>将显示指定PID进程打开的文件。

  6. strace:这个工具用于跟踪进程执行时的系统调用,对于程序调试和性能分析至关重要。例如,strace -p <pid>将跟踪并显示指定进程的系统调用。

  7. nicerenice:这两个命令用于调整进程的优先级。nice用于启动一个进程时设置其优先级,而renice则用于修改已经运行进程的优先级。例如,renice 10 -p <pid>会将指定PID的进程优先级调整为10。

这些工具是Linux上进行进程查看和管理的基石,通过它们可以实现对系统进程的精细化控制和监控。掌握它们的使用,可以帮助您更好地理解和管理Linux系统的运行状态。

2. C语言编码获取进程信息
2.1 读取comm线程名字

在C语言中,可以通过遍历/proc目录并读取每个进程目录下的comm文件来获取所有进程的名称。以下是一个简单的示例代码,它将列出/proc下所有进程的comm信息:

#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
    DIR           *dir;
    struct dirent *entry;
    char           path[512];
    char           comm[256];
    FILE          *fp;

    // 打开/proc目录
    dir = opendir("/proc");
    if (dir == NULL) {
        perror("opendir failed");
        exit(EXIT_FAILURE);
    }

    // 遍历目录项
    while ((entry = readdir(dir)) != NULL) {
        // 检查目录项名称是否为数字(进程ID)
        if (entry->d_type == DT_DIR && strtol(entry->d_name, NULL, 10) > 0) {
            // 构建comm文件的路径
            snprintf(path, sizeof(path), "/proc/%s/comm", entry->d_name);

            // 打开comm文件
            fp = fopen(path, "r");
            if (fp == NULL) {
                continue;
            }

            // 读取comm文件的内容(进程名称)
            if (fgets(comm, sizeof(comm), fp) != NULL) {
                // 移除换行符并打印进程名称
                comm[strcspn(comm, "\n")] = 0;
                printf("PID: %s, Comm: %s\n", entry->d_name, comm);
            }

            // 关闭comm文件
            fclose(fp);
        }
    }

    // 关闭/proc目录
    closedir(dir);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

此程序执行以下步骤:

  1. 打开/proc目录。
  2. 遍历目录项,检查目录名称是否为数字,因为进程目录总是以数字命名。
  3. 对于每个进程目录,构建指向其comm文件的路径。
  4. 打开comm文件并读取进程名称。
  5. 打印进程ID和名称。
  6. 关闭已打开的文件和目录。

我们在ubuntu环境下编译和该程序,输出如下(可以读出进程的名字):

ubuntu->cs-test:$ gcc -o read-comm read-comm.c 
ubuntu->cs-test:$ ./read-comm 
PID: 1, Comm: systemd
PID: 2, Comm: kthreadd
PID: 3, Comm: rcu_gp
PID: 4, Comm: rcu_par_gp
PID: 5, Comm: netns
......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
2.2 读取cmdline命令行文本

在Linux系统中,/proc目录是一个虚拟的文件系统,它提供了一个接口来访问内核数据结构。它不仅包含系统信息,还包含了每个进程信息的目录,例如/proc/[pid],其中[pid]是进程ID。在每个进程的目录下,有一个名为cmdline的文件,它包含了启动该进程的命令行参数。

要用C语言编码获取指定进程的cmdline信息,可以按照以下步骤编写代码:

  1. 构造/proc/[pid]/cmdline文件的路径。
  2. 打开这个文件。
  3. 读取文件内容。
  4. 关闭文件。

以下是一个例子,演示了如何获取进程ID为1234的cmdline信息:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

int main(int argc, char **argv)
{
    int    pid, i;
    FILE  *file;
    char   path[256], cmdline[PATH_MAX + 1];
    size_t size;

    // 读取命令行参数
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <pid>\n", argv[0]);
        return 1;
    }
    pid = atoi(argv[1]);

    // 构造文件路径
    snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);

    // 打开文件
    file = fopen(path, "r");
    if (file == NULL) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }

    // 读取文件内容
    size = fread(cmdline, 1, PATH_MAX, file);
    if (size == 0) {
        perror("fread");
        exit(EXIT_FAILURE);
    }

    // 将中间间隔的'\0'变成' '
    for (i = 0; i < size; i++) {
        if (cmdline[i] == '\0') {
            cmdline[i] = ' ';
        }
    }

    // 添加字符串终止符
    cmdline[size] = '\0';

    // 输出cmdline信息
    printf("The command line for PID %d is: %s\n", pid, cmdline);

    // 清理资源
    fclose(file);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

这段代码首先定义了进程ID并构建了相应的文件路径。使用fopen打开cmdline文件。之后,使用fread读取文件内容,并在末尾添加空字符以形成一个标准的字符串。最后,输出命令行信息,释放内存,关闭文件。

请注意,cmdline文件的内容使用空字符分隔命令行参数,所以上面代码中进行了转换,使用空格进行输出。下面是实际运行情况:

ubuntu->cs-test:$ gcc -o read-cmdline read-cmdline.c 
ubuntu->cs-test:$ ./read-cmdline 1911678
The command line for PID 1911678 is: sshd: ubuntu@pts/0   
ubuntu->cs-test:$ ./read-cmdline 1949309
The command line for PID 1949309 is: /home/ubuntu/.vscode-server/extensions/ms-vscode.cpptools-1.18.5-linux-x64/bin/cpptools-srv 1949199 {EDAFE4D2-4BEC-4A09-9466-66C5A7850042} 
  • 1
  • 2
  • 3
  • 4
  • 5
2.3 读取exe可执行程序路径

为了获取Linux系统中/proc目录下所有进程的可执行程序路径,我们可以编写一个C程序来遍历/proc目录,查找所有的数字命名子目录(这些通常代表进程ID),然后对每个进程读取其exe链接文件,该链接指向进程的可执行文件路径。

在Linux系统中,/proc/[pid]/exe是一个符号链接,指向启动该进程的可执行文件。

以下是实现这一功能的C语言代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <libgen.h>

int main()
{
    DIR           *proc;
    struct dirent *entry;
    char           exe_path[PATH_MAX];
    char           filepath[PATH_MAX];
    ssize_t        len;

    // 打开/proc目录
    if ((proc = opendir("/proc")) == NULL) {
        perror("opendir");
        exit(EXIT_FAILURE);
    }

    // 遍历/proc目录
    while ((entry = readdir(proc)) != NULL) {
        // 检查目录名是否为数字(代表进程ID)
        if (entry->d_type == DT_DIR &&
            strspn(entry->d_name, "0123456789") == strlen(entry->d_name)) {
            // 构造链接路径
            snprintf(filepath, sizeof(filepath), "/proc/%s/exe", entry->d_name);

            // 读取符号链接(即可执行文件的路径)
            if ((len = readlink(filepath, exe_path, sizeof(exe_path) - 1)) != -1) {
                exe_path[len] = '\0';    // 确保字符串以NULL结尾
                // 获取basename和dirname
                char *base = basename(exe_path);
                char *dir  = dirname(exe_path);
                printf(
                    "PID: %s - EXE: %s, BASE: %s, DIR: %s\n", entry->d_name, exe_path, base, dir);
            } else {
                perror("readlink");
            }
        }
    }

    // 关闭/proc目录
    closedir(proc);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

本段代码首先打开/proc目录,然后使用readdir函数遍历每个条目。对于每个条目,如果它是一个目录并且它的名字只包含数字,那么它很可能代表一个进程ID。对于这样的目录,代码构造了一个指向exe链接的路径,然后使用readlink函数尝试读取该链接的目标路径,即可执行文件的路径。成功读取后,将路径打印到标准输出。

需要注意的是,由于涉及到读取系统目录和链接,运行这个程序可能需要相应的权限,通常需要root权限才能读取所有进程的exe路径。此外,由于进程可能在任何时候结束,所以该程序可能无法捕获到系统中的瞬时进程。

下面是实际运行结果(可以看到很多权限报错,这是因为内核线程和一些root进程需要权限,可以用sudo运行):

ubuntu->cs-test:$ gcc -o read-exe read-exe.c 
ubuntu->cs-test:$ ./read-exe
......
readlink: Permission denied
readlink: Permission denied
readlink: Permission denied
PID: 1911597 - EXE: /usr/lib/systemd, BASE: systemd, DIR: /usr/lib/systemd
readlink: Permission denied
readlink: Permission denied
PID: 1911679 - EXE: /usr/bin, BASE: bash, DIR: /usr/bin
readlink: Permission denied
readlink: Permission denied
PID: 1948965 - EXE: /usr/bin, BASE: bash, DIR: /usr/bin
PID: 1949010 - EXE: /usr/bin, BASE: dash, DIR: /usr/bin
PID: 1949074 - EXE: /home/ubuntu/.vscode-server/bin/903b1e9d8990623e3d7da1df3d33db3e42d80eda, BASE: node, DIR: /home/ubuntu/.vscode-server/bin/903b1e9d8990623e3d7da1df3d33db3e42d80eda
PID: 1949131 - EXE: /home/ubuntu/.vscode-server/bin/903b1e9d8990623e3d7da1df3d33db3e42d80eda, BASE: node, DIR: /home/ubuntu/.vscode-server/bin/903b1e9d8990623e3d7da1df3d33db3e42d80eda
......
ubuntu->cs-test:$ sudo ./read-exe 
......
PID: 1961276 - EXE: /usr/bin, BASE: sleep, DIR: /usr/bin
readlink: No such file or directory
readlink: No such file or directory
readlink: No such file or directory
PID: 1962795 - EXE: /usr/bin, BASE: sudo, DIR: /usr/bin
PID: 1962796 - EXE: /usr/bin, BASE: sudo, DIR: /usr/bin
......
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

sudo运行很多报错No such file or directory, 其实这些进程时内核进程,所以符号链接文件指向是空的

2.4 如何判断内核进程

在Linux系统中,内核线程是一种特殊的进程,它们在内核空间运行,而不是在用户空间。内核线程可以通过检查它们的特征来区分,这些特征在/proc文件系统中可见。以下是一些区分用户进程和内核线程的方法:

  1. 命令行为空:对于内核线程,/proc/[pid]/cmdline文件通常为空,因为它们不是由外部命令启动的。

  2. 没有关联的终端:内核线程通常没有关联的终端。你可以通过查看/proc/[pid]/stat文件中的TTY(终端)字段来检查这一点。如果这个值是0,则表示没有关联的终端。

  3. 父进程:内核线程的父进程通常是kthreadd(PID为2)。

  4. 进程目录的task子目录:对于用户进程,/proc/[pid]/task目录包含该进程的所有线程。内核线程通常只有一个线程,所以这个目录下通常只有一个目录,其名字与PID相同。

  5. /proc/[pid]/exe链接:对于用户进程,/proc/[pid]/exe是一个到可执行文件的符号链接。对于内核线程,exe通常不存在或者链接无效。

  6. 名字的形式:内核线程的名字通常在/proc/[pid]/comm文件中以方括号包围,例如[kthreadd]

2.5 修改comm和cmdline信息

在Linux系统中,进程的名称可以通过两个属性来表示:commcmdlinecomm表示进程的简短名称,通常与可执行文件的名称相同,而cmdline则包含了启动该进程时传递给它的命令行参数。

  • comm:这个名称对应于/proc/[pid]/comm文件,包含了进程的名称。这个名称是进程的可执行文件名的最后16个字符(在Linux内核版本2.6.33之后,长度从15个字符扩展到了16个字符,包括终止的null字符)。

  • cmdline:这个文件/proc/[pid]/cmdline包含了进程启动时的命令行参数,参数之间由null字符分隔。

要修改进程的commcmdline,可以编写C语言程序直接写入到相应的/proc/self/目录下的文件。对于comm,可以调用prctl函数,对于cmdline,需要直接修改argv[]数组的数据。以下是一个示例:

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/prctl.h>

int main(int argc, char **argv)
{
    char        old_comm[16], comm[16], cmdline[64];
    const char *new_comm, *new_cmdline;
    size_t      read_bytes, cmdline_len;
    FILE       *cmdline_file;

    // 获取旧的comm
    if (prctl(PR_GET_NAME, old_comm) < 0) {
        perror("prctl get name");
        return 1;
    }

    // 修改comm
    new_comm = "my-new-comm";
    if (prctl(PR_SET_NAME, new_comm) < 0) {
        perror("prctl set name");
        return 1;
    }

    // 验证修改
    if (prctl(PR_GET_NAME, comm) < 0) {
        perror("prctl get name");
        return 1;
    }
    printf("Comm change: %s => %s\n", old_comm, comm);

    // 修改cmdline
    new_cmdline = "fake-name";
    cmdline_len = strlen(new_cmdline) + 1;
    // 直接写入到arv[0]的内存空间
    strncpy(argv[0], new_cmdline, cmdline_len);

    // 读取验证修改
    cmdline_file = fopen("/proc/self/cmdline", "r");
    if (!cmdline_file) {
        perror("fopen cmdline");
        return 1;
    }

    read_bytes  = fread(cmdline, 1, sizeof(cmdline) - 1, cmdline_file);
    fclose(cmdline_file);
    if (read_bytes > 0) {
        for (size_t i = 0; i < read_bytes; ++i) {
            if (cmdline[i] == '\0') {
                cmdline[i] = ' ';    // 替换null字符以打印
            }
        }
        printf("New cmdline: %s\n", cmdline);
    }

    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

请注意以下几点:

  1. 修改comm通过prctl系统调用完成,参数PR_SET_NAME用于设置新的comm值。
  2. 出于安全和稳定性的考虑,不是所有的环境都允许进程修改自己的cmdline。另外,cmdline的修改通常在进程启动初期完成,一旦进程开始执行,就不推荐修改,因为这可能会迷惑正在监视进程状态的系统管理工具和用户。
  3. 修改这些信息通常用于使进程在系统监视工具中更易于识别,但应谨慎使用,以避免误导或带来安全风险。
  4. 进程可以通过修改其内存空间中的参数来影响显示在cmdline中的内容。这通常是通过修改argv参数实现的,因为/proc/[pid]/cmdline是根据argv的内容生成的。但是这种做法并不常见,也不推荐使用,因为它可能会导致与其他程序的不兼容性,甚至可能会违反安全最佳实践。

下面是实际编译运行的结果:

ubuntu->cs-test:$ gcc -o change-self change-self.c 
ubuntu->cs-test:$ ./change-self 
Comm change: change-self => my-new-comm
New cmdline: fake-name elf 
  • 1
  • 2
  • 3
  • 4

可以看到,成功改变了commcmdline信息,从ps和top等命令看到的进程信息也会有变化

2.6 实现一个简易的killall工具

下面是一个简单的killall工具,可以kill掉指定可执行文件的进程,通过exe确定(类似pidof -x)进程是否属于目标进程。

/*
 * SPDX-License-Identifier: BSD-3-Clause
 *
 * Copyright (c) 2024 Once Day <once_day@qq.com>, All rights reserved.
 *
 * @FilePath: /cs-test/my-killall.c
 * @Author: Once Day <once_day@qq.com>.
 * @Date: 2024-02-22 10:16
 * @info: Encoder=utf-8,Tabsize=4,Eol=\n.
 *
 * @Description:
 *  简单的进程查找程序
 *
 * @History:
 *
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <signal.h>
#include <limits.h>
#include <dirent.h>
#include <ctype.h>
#include <errno.h>
#include <libgen.h>

/* 判断一个目录名是否是纯数字 */
static bool is_pid_directory(const char *dir_name)
{
    while (*dir_name) {
        if (!isdigit(*dir_name)) {
            return false;
        }
        dir_name++;
    }
    return true;
}

/* 在检索进程对象时遇到错误, 输出对应的信息用于定位 */
static void print_proc_error(const char *dir_name, const char *msg, int32_t err_code)
{
    FILE *comm_file;
    char *newline;
    char  path[256], comm[256];

    snprintf(path, sizeof(path), "/proc/%s/comm", dir_name);
    comm[0]   = '\0';
    comm_file = fopen(path, "r");
    if (comm_file) {
        if (fgets(comm, sizeof(comm), comm_file) != NULL) {
            /* Remove newline if present */
            newline = strchr(comm, '\n');
            if (newline) {
                *newline = '\0';
            }
        }
        fclose(comm_file);
    }

    /* 如果没有读到任何字符, 则提示未知进程名 */
    if (comm[0] == '\0') {
        snprintf(comm, sizeof(comm), "(unknown process name)");
    }

    /* 打印错误日志信息 */
    printf("%s [%s(%s)] [%s(%d)].\n", msg, comm, dir_name, strerror(err_code), err_code);

    return;
}

/* 读取exe可执行文件信息, 判断是否为目标进程名, 全匹配 */
bool check_proc_exe(const char *dir_name, const char *proc_name)
{
    size_t  bytes_read, i, seg;
    ssize_t link_len;
    FILE   *cmdline_file;
    char   *exe_name;
    char    proc_path[256], exe_path[PATH_MAX + 1];

    /* /proc/[pid]/exe是一个符号链接文件, 内核进程exe链接文件不存在 */
    snprintf(proc_path, sizeof(proc_path), "/proc/%s/exe", dir_name);
    link_len = readlink(proc_path, exe_path, sizeof(exe_path) - 1);
    if (link_len == -1) {
        /* note: 内核进程无法读取exe link文件, 只考虑用户空间进程, 即排除ENOENT的错误 */
        if (errno != ENOENT) {
            /* 如果非root用户, 可以排除权限错误警告EACCES */
            print_proc_error(dir_name, "Error reading executable link file", errno);
        }
        return false;
    }

    /* 确保字符串以 null 字符结尾 */
    exe_path[link_len] = '\0';
    /* printf("Executable path: %s\n", exe_path); */

    /* 处理路径字符串, 获取basename */
    exe_name = basename(exe_path);
    /* printf("Executable name: %s\n", exe_name); */

    if (strncmp(exe_name, proc_name, strlen(proc_name)) == 0) {
        printf("Process found: %s(%s) %ld.\n", proc_name, dir_name, strlen(proc_name));
        kill(atoi(dir_name), SIGKILL);
        return true;
    }

    return false;
}

/* 通过/proc文件系统查询当前设备上的进程信息 */
bool find_process_by_name(const char *proc_name)
{
    uint64_t       pid, count;
    DIR           *proc_dir;
    struct dirent *entry;

    /* 读取/proc内核信息虚拟目录信息 */
    proc_dir = opendir("/proc");
    if (proc_dir == NULL) {
        printf("Failed to open /proc directory, %s(%d).\n", strerror(errno), errno);
        return false;
    }

    /* 遍历整个/proc目录, 找到进程子目录, 逐个判断是否为目标进程 */
    count = 0;
    while ((entry = readdir(proc_dir)) != NULL) {
        if (entry->d_type == DT_DIR && is_pid_directory(entry->d_name)) {
            if (check_proc_exe(entry->d_name, proc_name) == true) {
                count++;
            }
        }
    }
    closedir(proc_dir);

    printf("Total %lu process(es) found.\n", count);
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("Usage: %s <process name>\n", argv[0]);
        return EXIT_FAILURE;
    }
    find_process_by_name(argv[1]);
    return EXIT_SUCCESS;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151

测试时,将2.5节的程序change-self加一个sleep函数,延迟1000s,change-self会改变自身的cmdline和comm内容,但是我们的killall仍然可以识别全部的change-self进程。

编译运行,执行如下:

ubuntu->cs-test:$ gcc -o change-self change-self.c 
ubuntu->cs-test:$ ./change-self & # 重复运行5次
[1] 1971781
Comm change: change-self => my-new-comm
New cmdline: fake-name elf 
ubuntu->cs-test:$ gcc -o my-killall my-killall.c 
ubuntu->cs-test:$ ./my-killall 
Usage: ./my-killall <process name>
ubuntu->cs-test:$ ./my-killall change-self
Error reading executable link file [kworker/u8:0-events_unbound(1971317)] [Permission denied(13)].
Process found: change-self(1971781) 11.
Process found: change-self(1971795) 11.
Process found: change-self(1971803) 11.
Process found: change-self(1971811) 11.
Process found: change-self(1971814) 11.
Error reading executable link file [sh(1973550)] [Permission denied(13)].
Error reading executable link file [tat_agent(2029982)] [Permission denied(13)].
Total 5 process(es) found.
[1]   Killed                  ./change-self
[2]   Killed                  ./change-self
[3]   Killed                  ./change-self
[4]-  Killed                  ./change-self
[5]+  Killed                  ./change-self
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

可以看到,my-killall成功找到5个change-self,并且kill了它们,实现了我们想要的功能

附录
附录1: opendir和readdir函数简介

在C语言中,opendirreaddir是两个用于操作目录的函数,它们定义在<dirent.h>头文件中。这两个函数通常用来遍历文件系统中的目录结构。

opendir函数用于打开一个目录,使其成为读取目录项的候选项。它的原型如下:

DIR *opendir(const char *name);
  • 1

这里DIR是一个表示目录流的类型,opendir函数接受一个参数,即要打开目录的路径字符串,并返回一个指向DIR类型的指针。如果打开目录失败,它会返回NULL,并且errno变量会被设置为相应的错误代码,以便可以进一步检查错误发生的原因。

readdir函数用于读取由opendir打开的目录流中的目录项。它的原型如下:

struct dirent *readdir(DIR *dirp);
  • 1

readdir接受一个指向DIR类型的指针,返回一个指向struct dirent结构的指针。这个结构包含了目录项的详细信息,最重要的字段包括:

  • d_name:一个字符数组,包含了目录项的名字。
  • d_type:一个字符,代表目录项的类型(如普通文件、目录等)。

readdir读取到目录流的末尾或发生错误时,将返回NULL。在使用readdir遍历目录时,它会记住当前的位置,所以每次调用时都会获取下一个目录项。需要注意的是,readdir返回的目录项顺序是不确定的,且可能会包含特殊的...目录项,分别表示当前目录和父目录。

使用opendirreaddir时,一旦目录项读取完成,应该使用closedir函数关闭目录流,释放资源。例如:

DIR *dir = opendir("/path/to/directory");
if (dir) {
    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        printf("%s\n", entry->d_name);
    }
    closedir(dir);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这个例子中,我们打开了指定路径的目录,然后循环读取每个目录项并打印其名称,最后关闭了目录流。opendirreaddir函数是目录操作中非常基础且重要的工具,它们使得在C语言中处理文件系统变得更加容易。

附录2: 介绍fread函数

fread函数用于从文件流中读取数据块,定义在<stdio.h>头文件中,它的原型如下:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 1

参数解释:

  • ptr:指向一个缓冲区的指针,用于存储读取的数据。
  • size:每个数据块的大小,以字节为单位。
  • nmemb:要读取的数据块的数量。
  • stream:指向FILE对象的指针,该FILE对象代表了一个打开的文件流。

fread函数从stream指定的文件流中读取nmemb项数据,每项数据的大小为size字节,存储在ptr指向的缓冲区中。函数返回成功读取的数据块数目,如果此数目与nmemb不相符,可能是因为发生了错误或达到了文件末尾。

fgets不同,fread是以二进制形式读取文件,不会在读取的数据后添加空终止符,也不会因为换行符或文件末尾而停止读取。它通常用于读取结构体数据或二进制文件。

一个简单的fread使用示例可能如下:

FILE *file = fopen("example.bin", "rb");
if (file) {
    char buffer[1024];
    size_t bytesRead = fread(buffer, sizeof(char), sizeof(buffer), file);
    fclose(file);
    // 处理读取的数据...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这个例子中,我们打开了一个名为"example.bin"的二进制文件,然后尝试读取到缓冲区buffer中,并记录实际读取的字节数。

fread非常适合读取二进制数据和大块数据

附录3: 介绍fgets函数

fgets函数用于从文件中读取字符串,定义在<stdio.h>头文件中,它的原型如下:

char *fgets(char *s, int size, FILE *stream);
  • 1

参数解释:

  • s:指向一个字符数组的指针,用于存储读取的字符串。
  • size:指定最多读取的字符数,包括空终止字符\0
  • stream:指向FILE对象的指针,该FILE对象代表了一个打开的文件流。

fgets函数会从指定的文件流stream中读取字符,直到发生以下三种情况之一:

  1. 读取了size-1个字符;
  2. 遇到换行符'\n'
  3. 到达了文件末尾EOF。

读取停止后,fgets会在字符串的末尾添加一个空终止符\0。如果读取成功,fgets返回s;如果遇到文件末尾或发生错误,fgets返回NULL

fgets是一个安全的读取字符串的函数,因为它允许指定缓冲区的大小,防止缓冲区溢出。

附录4: 介绍readlink函数

readlink函数是一个在类UNIX操作系统中用于读取符号链接(symbolic link)内容的系统调用。符号链接是一种特殊类型的文件,它包含的是指向另一个文件路径的引用。不同于硬链接,符号链接可以跨文件系统,并且链接的目标可以是任何类型的文件,包括目录。

函数的原型定义在 <unistd.h> 头文件中,其用途主要是获取符号链接所指向的原始路径。当你想要知道一个符号链接指向何处时,就可以使用 readlink 函数。

函数原型如下:

#include <unistd.h>
ssize_t readlink(const char *restrict path, char *restrict buf, size_t bufsize);
  • 1
  • 2
  • path 参数是指向符号链接的路径字符串。
  • buf 参数是一个字符数组,用来存储符号链接指向的路径。
  • bufsize 参数指定了缓冲区 buf 的大小。

调用 readlink 时,它会将 path 指向的符号链接所引用的路径读取到 buf 中,并返回读取的字节数。如果成功,返回值是写入 buf 的字节数(不包含空终止字符);如果失败,返回 -1 并设置 errno 来表示错误。

readlink 函数不会在 buf 后追加空字符,所以在处理返回的数据时,你可能需要根据返回值在 buf 后手动添加空字符,以确保它是一个标准的字符串。

以下是 readlink 函数可能遇到的一些错误情况:

  • EACCES:路径中的目录不可搜索。
  • EINVALpath 不是一个符号链接。
  • EIO:输入输出错误,无法读取链接。
  • ELOOP:解析 path 中的符号链接时遇到太多层的符号链接。
  • ENAMETOOLONGpath 太长。
  • ENOENTpath 指向的符号链接不存在。
  • ENOTDIRpath 的前缀不是目录。
  • EFAULTpathbuf 指向了不可访问的内存区域。

readlink 是处理符号链接的常用函数,它可以帮助程序了解文件的真实路径,尤其是在处理文件链接时,它提供了一种有效的方法来避免进入符号链接引起的循环。在文件系统操作中,readlink 函数是实现某些特定功能(比如寻找程序的实际安装位置)时不可或缺的工具。

附录5: 介绍kill函数

kill函数是一种在UNIX-like操作系统中用于向进程发送信号的系统调用。它的作用是向指定的进程或进程组发送一个信号,信号可以用来中断、终止甚至改变进程的行为。kill函数的原型定义在头文件 <signal.h> 中,它的使用在C语言编程中相当常见,尤其是在需要进行进程控制和管理的场景下。

函数原型如下:

#include <signal.h>
int kill(pid_t pid, int sig);
  • 1
  • 2

这里的pid_t是一个用于进程ID的数据类型,而sig是希望发送的信号的编号。如果调用成功,kill函数返回0;如果失败,返回-1,并设置errno以指示错误原因。

kill函数的参数pid可以是以下几种情况之一:

  • pid > 0:信号被发送到进程ID为pid的进程。
  • pid == 0:信号被发送到与发送进程属于同一个进程组的所有进程。
  • pid < -1:信号被发送到进程组ID为-pid的所有进程。
  • pid == -1:信号被发送到所有发送进程有权限发信号的进程,除了系统进程和调用进程。

sig参数则指定了要发送的信号类型,这些信号包括但不限于:

  • SIGINT:终止进程(可中断)。
  • SIGKILL:立即终止进程(不可中断)。
  • SIGTERM:请求终止进程(可中断和处理)。
  • SIGSTOP:停止(暂停)进程的执行(不可中断)。
  • SIGCONT:如果进程被停止,让它继续执行。

kill函数的强大之处在于它提供了一个简洁的方式来控制进程的行为。例如,当用户在命令行中按下Ctrl+C时,通常会发送SIGINT信号给前台进程组中的所有进程,这通常会导致进程的终止。而在编写守护进程或者需要清理资源的场景中,可能会捕捉SIGTERM信号来执行清理操作后再退出进程。

值得注意的是,有些信号是不能被进程捕捉或忽略的,比如SIGKILLSIGSTOP,这是为了确保系统管理员能够控制系统中的进程。而其他信号,比如SIGTERM,则可以被进程捕捉,以便进行适当的清理工作。

使用kill函数时,需要具有相应的权限,通常是需要进程的拥有者或者超级用户权限才能向进程发送信号。如果权限不足,kill函数会失败,并且errno会被设置为EPERM

总的来说,kill函数是进程间通信和控制的重要手段,它允许进程间以及操作系统与进程之间进行有效的信号传递。程序员在使用kill函数时应当小心谨慎,以确保正确、合理地管理进程。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/142933
推荐阅读
相关标签
  

闽ICP备14008679号