跳至内容

Jixun's Blog 填坑还是开坑,这是个好问题。

使用 C + FastCGI 制作一个提供当前机器基本信息的服务

前言 #

想着做个简单的主从程序用于来监控所有机器的简单数据,其中主程序作为工作程序的一个前端并缓存,避免用户直接访问到后端的 API。

这篇文章是关于这个工作程序,其它部分不开放。

FastCGI 进程的期待访问次数为每 10 秒最多请求 1 次,由下游的后端来负责数据缓存。

部署的主程序前端可以在stats.jixun.uk查阅。

环境 #

目前使用了一系列不同的操作系统,分别有 Alpine LinuxUbuntuCentOS 7

每个环境都有自己的依赖,我做了个简单的脚本来配置上述系统的环境(要求 which 指令可用):

setup_apt()
{
  apt update
  apt install -y gcc make libfcgi libfcgi-dev spawn-fcgi
}

setup_apk()
{
  apk update
  apk add gcc musl-dev make fcgi fcgi-dev spawn-fcgi
}

setup_yum()
{
  yum -y install gcc make fcgi fcgi-devel spawn-fcgi
}

which apt && setup_apt
which apk && setup_apk
which yum && setup_yum

代码 #

FastCGI 相关的信息可以从其存档的官网获取,在这里能找到一个简单的例子

最后,因为是流式输出内容,而且没有什么需要转义的字符串内容,这个项目并没有用到处理 JSON 的第三方库,而是直接输出 JSON 格式的文本。

#include "fcgi_stdio.h"

#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <sys/statvfs.h>

#define JSON_KEY(str) "\"" str "\""
#define IS_DIGIT(c) (c >= '0' && c <= '9')

// Do not use shared_buff if multi-threaded.
char shared_buff[4096];

// CPU INFO

void print_cpu()
{
  printf(JSON_KEY("cpu") ":");
  FILE* f = fopen("/proc/loadavg", "r");
  fread(shared_buff, 1024, 1, f);
  fclose(f);

  char* p = shared_buff;
  while(*p != ' ') p++;
  *p = 0;

  printf("%s", shared_buff);
}

// RAM INFO

// Ram Keyword struct
struct kwd {
  const char* word;
  int len;
};

struct kwd ramKwds[] = {
  {"MemTotal", 8},
  {"MemFree",  7},
  {"Cached",   6},

  {"SwapTotal",  9},
  {"SwapFree",   8},
  {"SwapCached", 10},
};
#define RAM_KWD_N (sizeof(ramKwds)/sizeof(ramKwds[0]))

void print_ram()
{
  printf(JSON_KEY("ram") ":{");
  
  FILE* f = fopen("/proc/meminfo", "r");
  fread(shared_buff, 4096, 1, f);
  fclose(f);

  int wrote = 0;
  char* p = shared_buff;
  while(*p)
  {
    char* line = p;
    while(*p && *p != '\n') p++;
    if (!*p) break;
    *p++ = 0;

    for (int i = 0; i < RAM_KWD_N; i++) {
      if (memcmp(ramKwds[i].word, line, ramKwds[i].len) == 0) {
        if (wrote) putchar(',');

        printf("\"%s\":", ramKwds[i].word);

        // skip past ':' char
        line += ramKwds[i].len + 1;

        // skip whitespace
        while(*line == ' ') {
          line++;
        }
        while(IS_DIGIT(*line)) {
          putchar(*line++);
        }

        wrote = 1;
      }
    }
  }
  putchar('}');
}

char* storages[] = {
  "/"
};
#define STORAGE_COUNT (sizeof(storages)/sizeof(storages[0]))

void print_storage()
{
  printf(JSON_KEY("drives") ":[");

  struct statvfs buf;
  int wrote = 0;
  for (int i = 0; i < STORAGE_COUNT; i++) {
    // see: https://github.com/php/php-src/blob/
    //      001d43444944df0bef4e33a1876ba2818c188e11/ext/standard/filestat.c#L251
    if (statvfs(storages[i], &buf)) continue;

    uint64_t block_size  = buf.f_frsize ? buf.f_frsize : buf.f_bsize;
    uint64_t bytes_free  = block_size * buf.f_bavail;
    uint64_t bytes_total = block_size * buf.f_blocks;

    if (wrote) putchar(',');

    printf("{"
      JSON_KEY("mnt")   ":\"%s\","
      JSON_KEY("free")  ":%" PRIu64 ","
      JSON_KEY("total") ":%" PRIu64 "}"
      , storages[i], bytes_free, bytes_total
    );

    wrote = 1;
  }
  putchar(']');
}

int main(void) {
  while (FCGI_Accept() >= 0) {
    printf("Content-type: application/json\r\n");
    printf("\r\n");
    putchar('{');
    print_cpu();
    putchar(',');
    print_ram();
    putchar(',');
    print_storage();
    putchar('}');
    }

    return 0;
}

编译时带上 -lfcgi 即可。我使用的编译语句如下:

gcc main.c -Wall -Wpedantic -std=c11 -lfcgi -O2 -o stats-worker.cgi

运行 #

因为 nginx 不支持直接启动 fastcgi 程序,因此利用 spawn-fcgi 来启动一个监听后台。

带上 -n 使其不要 fork,方便在 systemdopenrc 环境下管理进程的启动与中止。

# run as root and let spawn-fcgi drop privilege later.
spawn-fcgi -n -u www-data -g www-data -f /srv/slave.cgi -s /run/slave.sock -P /run/slave.pid

知识共享许可协议 本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

评论区