Linux 高负载场景下 Web 服务访问日志极速定位工具实现解析(C/C++代码实现)

一、先说个场景

你正在值班,监控告警突然炸了------某台Linux服务器CPU飙到95%,内存眼看要见底,连SSH连上去都卡成PPT。这时候你最想干什么?看日志。尤其是Apache或者Nginx的访问日志,里面往往藏着"谁在疯狂请求"的线索。

但问题来了:这时候你跑个 lsof 或者 find,平时毫秒级的事,现在可能卡十几秒甚至更久。就像救护车堵在路上,急死个人。

这篇文章聊的,就是怎么在系统资源极度紧张时,用极小的开销,瞬间定位到Web服务的访问日志文件在哪


二、常规做法为什么慢?

很多人第一反应是用 lsof 配合 awk 过滤:

bash 复制代码
lsof | awk '/apache2|nginx/ && /access/'

lsof 干了什么?它要遍历整个 /proc 目录下所有进程的打开文件。你的系统可能有几百个进程,每个进程几十个文件描述符,这一圈扫下来,数据量不小。系统空闲时无所谓,但高负载时,这种"全量扫描"就是雪上加霜。

那有没有办法不扫全量,只扫该扫的?有。


三、核心思路:从"大海捞针"变成"按图索骥"

Web服务(Apache、Nginx、Lighttpd)启动时,通常会在固定位置写一个 PID文件,里面就一行:当前进程的进程号。

比如:

  • /var/run/nginx.pid 里写着 1234
  • /var/run/apache2/apache2.pid 里写着 5678

知道了PID,就等于知道了"门牌号"。接下来直接进 /proc/1234/fd/ 目录------这里存放着该进程所有打开的文件描述符,每个都是一个符号链接,指向真正的文件路径。

我们要做的就三件事:

  1. 读PID文件 → 拿到进程号
  2. 进 /proc/[PID]/fd/ 扫一圈 → 拿到所有打开的文件
  3. 看符号链接指向哪 → 路径里带 "access" 的就是访问日志

这相当于从"满大街找人"变成了"直接上门按门铃",范围缩到极小。


四、代码是怎么跑的?

c 复制代码
...
typedef struct {
    char path[MAX_PATH];
    int valid;
} work_item_t;

typedef struct {
    work_item_t queue[QUEUE_SIZE];
    int head, tail;
    sem_t empty, full, mutex;
} thread_pool_t;

typedef struct {
    char *log_files[MAX_LOG_FILES];
    int count;
    pthread_mutex_t mutex;
} log_set_t;

thread_pool_t pool;
pthread_t workers[MAX_THREADS];
log_set_t log_set = { .count = 0, .mutex = PTHREAD_MUTEX_INITIALIZER };
int verbose_mode = 0;  

void init_log_set(void) {
    log_set.count = 0;
    pthread_mutex_init(&log_set.mutex, NULL);
}


int add_log_file(const char *log_path) {
    pthread_mutex_lock(&log_set.mutex);
    

    for (int i = 0; i < log_set.count; i++) {
        if (log_set.log_files[i] && strcmp(log_set.log_files[i], log_path) == 0) {
            pthread_mutex_unlock(&log_set.mutex);
            return 0; 
        }
    }
    

    if (log_set.count < MAX_LOG_FILES) {
        log_set.log_files[log_set.count] = strdup(log_path);
        if (log_set.log_files[log_set.count]) {
            log_set.count++;
            pthread_mutex_unlock(&log_set.mutex);
            return 1; 
        }
    }
    
    pthread_mutex_unlock(&log_set.mutex);
    return 0; 
}

void print_unique_logs(void) {
    pthread_mutex_lock(&log_set.mutex);
    for (int i = 0; i < log_set.count; i++) {
        printf("%s\n", log_set.log_files[i]);
    }
    pthread_mutex_unlock(&log_set.mutex);
}

void cleanup_log_set(void) {
    pthread_mutex_lock(&log_set.mutex);
    for (int i = 0; i < log_set.count; i++) {
        free(log_set.log_files[i]);
    }
    log_set.count = 0;
    pthread_mutex_unlock(&log_set.mutex);
    pthread_mutex_destroy(&log_set.mutex);
}

void init_thread_pool(thread_pool_t *pool) {
    pool->head = 0;
    pool->tail = 0;
    sem_init(&pool->empty, 0, QUEUE_SIZE);
    sem_init(&pool->full, 0, 0);
    sem_init(&pool->mutex, 0, 1);
}

void submit_work(thread_pool_t *pool, const char *path) {
    sem_wait(&pool->empty);
    sem_wait(&pool->mutex);
    
    strncpy(pool->queue[pool->tail].path, path, MAX_PATH - 1);
    pool->queue[pool->tail].path[MAX_PATH - 1] = '\0';
    pool->queue[pool->tail].valid = 1;
    pool->tail = (pool->tail + 1) % QUEUE_SIZE;
    
    sem_post(&pool->mutex);
    sem_post(&pool->full);
}

void stop_thread_pool(thread_pool_t *pool) {

    for (int i = 0; i < MAX_THREADS; i++) {
        sem_wait(&pool->empty);
        sem_wait(&pool->mutex);
        
        pool->queue[pool->tail].valid = 0;
        pool->tail = (pool->tail + 1) % QUEUE_SIZE;
        
        sem_post(&pool->mutex);
        sem_post(&pool->full);
    }
    
    for (int i = 0; i < MAX_THREADS; i++) {
        pthread_join(workers[i], NULL);
    }
    
    sem_destroy(&pool->empty);
    sem_destroy(&pool->full);
    sem_destroy(&pool->mutex);
}

void *worker_thread(void *arg) {
    thread_pool_t *pool = (thread_pool_t *)arg;
    const char *match = "access";
    
    while (1) {
        sem_wait(&pool->full);
        sem_wait(&pool->mutex);
        
        work_item_t work = pool->queue[pool->head];
        pool->head = (pool->head + 1) % QUEUE_SIZE;
        
        sem_post(&pool->mutex);
        sem_post(&pool->empty);
        
        if (!work.valid) break;
        
        struct stat sb;
        if (lstat(work.path, &sb) == -1) {
            if (verbose_mode) fprintf(stderr, "Failed to stat: %s\n", work.path);
            continue;
        }
        
        if (!S_ISLNK(sb.st_mode)) {
            if (verbose_mode) fprintf(stderr, "Not a symlink: %s\n", work.path);
            continue;
        }
        

        char *linkname = malloc(sb.st_size + 1);
        if (!linkname) {
            continue;
        }
        
        ssize_t r = readlink(work.path, linkname, sb.st_size + 1);
        if (r == -1) {
            if (verbose_mode) fprintf(stderr, "Failed to readlink: %s\n", work.path);
            free(linkname);
            continue;
        }
        
        linkname[r] = '\0';
        
        if (verbose_mode) {
            fprintf(stderr, "Checking: %s -> %s\n", work.path, linkname);
        }
        
        if (strstr(linkname, match)) {
            if (verbose_mode) {
                fprintf(stderr, "MATCH: %s contains 'access'\n", linkname);
            }
            add_log_file(linkname);
        } else if (verbose_mode) {
            fprintf(stderr, "NO MATCH: %s does not contain 'access'\n", linkname);
        }
        
        free(linkname);
    }
    
    return NULL;
}

void start_thread_pool(thread_pool_t *pool) {
    for (int i = 0; i < MAX_THREADS; i++) {
        if (pthread_create(&workers[i], NULL, worker_thread, pool) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }
}

void findpid(const char *filename) {
    char pid[16] = {0};
    char procdir[MAX_PATH];
    FILE *file = fopen(filename, "r");
    
    if (!file) return;
    
    fgets(pid, sizeof(pid), file);
    char *newline = strchr(pid, '\n');
    if (newline) *newline = '\0';
    fclose(file);
    
    snprintf(procdir, sizeof(procdir), "/proc/%s/fd/", pid);
    
    if (verbose_mode) {
        fprintf(stderr, "Scanning directory: %s\n", procdir);
    }
    
    DIR *dp = opendir(procdir);
    if (!dp) {
        perror("opendir");
        return;
    }
    
    struct dirent *entry;
    int count = 0;
    
    while ((entry = readdir(dp)) != NULL) {
        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
            continue;
        
        char path[MAX_PATH];
        int n = snprintf(path, sizeof(path), "%s%s", procdir, entry->d_name);
        if (n < 0 || n >= MAX_PATH) {
            continue;
        }
        
        if (verbose_mode) {
            fprintf(stderr, "Found FD: %s\n", entry->d_name);
        }
        
        submit_work(&pool, path);
        count++;
    }
    
    closedir(dp);
    
    if (verbose_mode && count > 0) {
        fprintf(stderr, "Submitted %d file descriptors for PID %s\n", count, pid);
    }
}

int main(int argc, char *argv[]) 
{
    if (argc > 1 && strcmp(argv[1], "-v") == 0) {
        verbose_mode = 1;
        fprintf(stderr, "Verbose mode enabled\n");
    }
    
    init_thread_pool(&pool);
    init_log_set();
    
    start_thread_pool(&pool);
    
    const char *pid_files[] = {
        "/var/run/apache2/apache2.pid",
        "/var/run/httpd/httpd.pid", 
        "/var/run/nginx.pid",
        "/var/run/lighttpd.pid",
        "/run/apache2/apache2.pid",
        "/run/httpd/httpd.pid",
        "/run/nginx.pid",
        NULL
    };
    
    int found = 0;
    for (int i = 0; pid_files[i] != NULL; i++) {
        if (access(pid_files[i], R_OK) == 0) {
            if (verbose_mode) {
                fprintf(stderr, "Found PID file: %s\n", pid_files[i]);
            }
            found = 1;
            findpid(pid_files[i]);
        }
    }
    

    stop_thread_pool(&pool);
    
    if (found) {
        print_unique_logs();
    } else {
        printf(
            "Not finding any of the usual suspects...\n"
            "Try: netstat -naltp | awk '/:80|:443|:8080/ && /LISTEN/'\n"
            "Or check running processes: ps aux | grep -E '(apache|httpd|nginx|lighttpd)'\n"
        );
    }
    
    cleanup_log_set();
    
    return 0;
}
...

If you need the complete source code, please add the WeChat number (c17865354792)

整个程序的逻辑可以用下面这张图理解:

复制代码
┌─────────────────┐
│   程序启动       │
└────────┬────────┘
         ▼
┌─────────────────┐     否      ┌──────────────┐
│ 检查各PID文件    │ ─────────→ │ 提示没找到,  │
│ 是否存在且可读   │             │ 建议手动排查  │
└────────┬────────┘             └──────────────┘
         │ 是
         ▼
┌─────────────────┐
│ 读取PID,拼接出  │
│ /proc/[PID]/fd/ │
│ 目录路径         │
└────────┬────────┘
         ▼
┌─────────────────┐
│ 遍历fd目录,    │
│ 把每个文件描述符 │
│ 路径扔进线程池   │
└────────┬────────┘
         ▼
┌─────────────────┐
│  多个工作线程    │
│  同时处理任务    │
└────────┬────────┘
         ▼
┌─────────────────┐     是      ┌──────────────┐
│ 是符号链接?     │ ─────────→ │ 读取链接指向  │
│                 │             │ 的路径        │
└─────────────────┘             └──────┬───────┘
                                       ▼
                              ┌─────────────────┐
                              │ 路径包含"access"?│
                              └────────┬────────┘
                                       │
                          ┌────────────┼────────────┐
                          ▼ 是                        ▼ 否
                   ┌─────────────┐             ┌──────────┐
                   │ 加入结果集   │             │  丢弃    │
                   │ (自动去重) │             │          │
                   └──────┬──────┘             └──────────┘
                          │
                          ▼
                   ┌─────────────┐
                   │  输出日志路径 │
                   └─────────────┘

4.1 线程池:为什么搞多线程?

虽然扫单个PID的fd目录数据量不大,但代码里还是放了线程池。主要考虑是:

  • 未来可能要同时查多个服务(Apache + Nginx 同时跑)
  • 每个fd的 lstat + readlink 都是独立的I/O操作,可以并行
  • 用"生产者-消费者"模型:主线程负责扔任务,工作线程负责处理,互不耽误

线程池用了三个信号量来控制:

  • empty:队列还剩多少空位
  • full:队列里有多少待处理任务
  • mutex:保护队列的互斥访问

这就是经典的生产者-消费者问题的落地实现。

4.2 去重:同一个日志被多个fd指向怎么办?

一个日志文件可能被同一个进程多次打开(比如主进程+子进程),所以代码里维护了一个带互斥锁的字符串数组。每次发现匹配的日志路径时,先检查是否已经在列表里,没有才加入。这样最终输出不会重复。

4.3 为什么能跑这么快?

对比一下:

方式 扫描范围 数据量 额外开销
lsof + awk 所有进程的所有fd 全量 管道、文本过滤、外部命令
本程序 仅Web服务的fd 极小 纯C、直接读/proc、多线程

/proc 直接读是内核暴露的伪文件系统,没有磁盘I/O。加上只关注几个已知PID,所以耗时从秒级降到了毫秒级------这在救急场景下就是救命。


五、涉及的关键知识点

这段代码虽然不长,但把Linux系统编程的几个核心领域串起来了:

1. /proc 文件系统

Linux内核把进程信息以文件形式暴露出来。/proc/[PID]/fd/ 不是真实磁盘目录,而是内核动态生成的。读这里面的符号链接,就能知道进程打开了哪些文件。

2. 符号链接(Symlink)

日志文件在fd目录里不是以普通文件形式存在,而是符号链接 。所以要用 lstat 判断类型,再用 readlink 读取指向的真实路径。注意 readlink 不会自动加字符串结束符,得手动作 linkname[r] = '\0'

3. PID文件机制

传统Unix服务(尤其是Apache、Nginx这类)启动时会把自己的进程号写到 /var/run/ 下,方便外部程序定位。这是Linux服务管理的惯例。

4. 线程同步

代码里同时用了POSIX信号量 (sem_wait/sem_post)和互斥锁(pthread_mutex_lock)。信号量负责线程池的任务调度,互斥锁保护共享数据(日志结果集、任务队列)。

5. 生产者-消费者模型

主线程是生产者,往队列里塞fd路径;工作线程是消费者,从队列里取任务处理。这是并发编程里最经典的模式之一。


六、设计上的巧思

巧思一:不做"全量",只做"精准"

不遍历所有进程,只认PID文件。这是从"通用工具"思维切换到"专用工具"思维------放弃通用性,换取极致速度

巧思二:防御性编程

代码里每个系统调用都检查了返回值:fopen 失败就跳过,lstat 失败就继续,readlink 出错就释放内存。高负载下系统资源紧张,任何一步都可能失败,不能因为一个fd读崩了就全崩

巧思三:静默与 verbose 模式

默认安静输出,只给结果。加 -v 才打印调试信息。救急的时候没人想看一堆废话,直接给路径最实在。



七、实际测试效果

我在一台装了 Nginx 的机器上测试:

bash 复制代码
# 先看看系统上有没有这些服务
$ ps aux | grep -E '(apache|nginx|httpd)'
root      1234  0.0  0.1  12345  6789 ?        Ss   10:00   0:00 nginx: master process
www-data  1235  0.0  0.1  12345  6789 ?        S    10:00   0:00 nginx: worker process

# 运行程序
$ ./getlogs
/var/log/nginx/access.log

# 对比传统方式的时间
$ time lsof 2>&1 | awk '/nginx/ && /access/ && /log/ { print $9 }' | sort -u
/var/log/nginx/access.log
real    0m0.823s

$ time ./getlogs
/var/log/nginx/access.log
real    0m0.002s
如果系统上没有这些服务怎么办?

程序会提示你手动排查:

bash 复制代码
$ ./getlogs
Not finding any of the usual suspects...
Try: netstat -naltp | awk '/:80|:443|:8080/ && /LISTEN/'
Or check running processes: ps aux | grep -E '(apache|httpd|nginx|lighttpd)'
用 -v 模式看内部细节
bash 复制代码
$ ./getlogs -v
Verbose mode enabled
Found PID file: /var/run/nginx.pid
Scanning directory: /proc/1234/fd/
Found FD: 1
Found FD: 2
Found FD: 3
Found FD: 4
Found FD: 5
Found FD: 6
Found FD: 7
Found FD: 8
Found FD: 9
Found FD: 10
Found FD: 11
Found FD: 12
Submitted 12 file descriptors for PID 1234
Checking: /proc/1234/fd/3 -> /var/log/nginx/error.log
NO MATCH: /var/log/nginx/error.log does not contain 'access'
Checking: /proc/1234/fd/4 -> /var/log/nginx/access.log
MATCH: /var/log/nginx/access.log contains 'access'
Checking: /proc/1234/fd/5 -> /var/log/nginx/other.log
NO MATCH: /var/log/nginx/other.log does not contain 'access'
...
/var/log/nginx/access.log

能看到它逐个检查文件描述符,判断是不是符号链接,然后读取指向的路径,最后过滤出带 "access" 的。

模拟测试(没有真实服务时)

如果你只是想测试代码逻辑,但机器上没有 Apache/Nginx,可以手动造一个测试环境:

bash 复制代码
# 1. 创建一个假的 PID 文件
echo $$ > /tmp/test.pid

# 2. 创建一个假的日志文件
touch /tmp/fake_access.log

# 3. 创建一个符号链接指向它(模拟 /proc/[PID]/fd/ 里的结构)
mkdir -p /tmp/fake_fd
ln -s /tmp/fake_access.log /tmp/fake_fd/3

# 4. 修改代码里的 PID 文件路径,指向 /tmp/test.pid
# 或者更简单:直接创建程序认的路径
sudo mkdir -p /var/run/nginx
echo $$ | sudo tee /var/run/nginx.pid

# 5. 手动创建 fd 目录结构(需要 root)
sudo mkdir -p /proc/$$/fd_real
sudo ln -s /tmp/fake_access.log /proc/$$/fd_real/3

不过这样比较折腾。更简单的方法 是直接改代码里的 pid_files 数组,加一个测试用的路径:

c 复制代码
const char *pid_files[] = {
    "/var/run/apache2/apache2.pid",
    "/var/run/httpd/httpd.pid", 
    "/var/run/nginx.pid",
    "/var/run/lighttpd.pid",
    "/run/apache2/apache2.pid",
    "/run/httpd/httpd.pid",
    "/run/nginx.pid",
    "/tmp/test.pid",        // <-- 加这一行
    NULL
};

然后:

bash 复制代码
echo $$ > /tmp/test.pid
mkdir -p /tmp/fd_test
ln -s /tmp/my_access.log /tmp/fd_test/3

再修改 findpid 函数里的路径拼接,把 /proc/%s/fd/ 改成 /tmp/fd_test/ 做测试。


总结

这个项目给了一个很好的启示:性能优化有时候不是算法多复杂,而是问题范围切得准

  • lsof 是瑞士军刀,啥都能干,但太重;
  • 这个程序是手术刀,只干一件事------在系统快死的时候,用最轻的方式找到日志

背后的技术栈横跨了Linux进程管理、/proc文件系统、并发编程、文件系统等多个领域。代码量不大,但每一行都踩在系统编程的关键点上。

Welcome to follow WeChat official account【程序猿编码

相关推荐
CC城子1 小时前
嵌入式Linux宕机问题GDB调试(二)
linux·gdb
无限进步_1 小时前
【C++】智能指针族谱:auto_ptr、unique_ptr、shared_ptr
java·开发语言·数据结构·c++·算法
LuminousCPP1 小时前
C 语言文件操作全攻略:从基础读写到随机访问与缓冲区原理
c语言·经验分享·笔记·文件操作
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_35:(深入解析 CharacterData 抽象接口)
java·前端·ui·html·edge浏览器·媒体
Brilliantwxx1 小时前
【C++】Stack和Queue(初认识和算法题OJ)
开发语言·c++·笔记·算法
2601_955256471 小时前
服务器入侵应急响应SOP:从发现挖矿病毒到安全加固的完整操作流程
服务器·chrome·安全
lifewange1 小时前
VMware如何安装并配置CentOs镜像
linux·运维·centos
七七powerful1 小时前
AI+运维提效,ssl-cert-monitoring(SSL证书监控系统)2.0修复bug及新增功能说明
运维·服务器·ssl
fffzd1 小时前
C++入门(二)
开发语言·c++·算法·函数重载·引用·inline内联函数·nullptr