Skip to content
Published at:

改变程序行为的几种方式总结

对于文章的标题思考了一会儿,“程序传参的方式”,“和程序交互的方式”,我想表达的是,一个程序已经编译了,有哪些方式能去改变程序的行为

方式列表:

  • 程序传参
  • 环境变量
  • 配置文件
  • 加载动态库
  • CS模式,暴露接口(Domain socket、Net socket、Http、、、之类)

程序传参

参数跟在程序的后面:./a.out -x yyy,通过main函数的参数去获取,在terminal终端上敲的命令,基本都是使用的这种方式

c
#include <stdio.h>

// argc:是程序命令和参数总的个数
// argv:是程序命令和参数总的个数列表
int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; i++) {
        printf("args list[%d]: %s\n", i, argv[i]);
    }
    return 0;
}

环境变量

把参数已key=val的形式放在可执行程序的前面:key=val ./a.out,记得第一次见这种形式的方式是用来控制程序的输出日志级别,类似$ LOG_LEVEL=DEBUG ./a.out这样。原理是:在terminal终端上敲的命令,shell会fork进程,产生一个子进程,子程序会继承父程序的环境变量信息

  • terminal终端里可以通过env,printenv之类的命令去设置查看
  • 在代码中通过extern char **environ;变量去获取
  • 在代码中通过char *getenv(const char *name);函数去获取
  • 在代码中通过main函数的第三个参数去获取int main(int argc, char *argv[], char *env[])

envprintenv命令

这两个我就不讲了,看看man手册,有详细介绍

extern char **environ;变量

c
#include <stdio.h>

int main(int argc, char* argv[]) {
    extern char** environ;

    for (int i = 0; environ[i] != NULL; i++) {
        printf("environ list[%d]: %s\n", i, environ[i]);
    }

    return 0;
}

char *getenv(const char *name);函数

c
#include <stdio.h>

int main(int argc, char* argv[]) {
    char* val = getenv("PATH");
    printf("val = %s\n", val);
    return 0;
}

int main(int argc, char *argv[], char *env[])参数

老一点的项目里面可能会看到这样的main函数

c
#include <stdio.h>

int main(int argc, char* argv[], char* env[]) {
    for (int i = 0; env[i] != NULL; i++) {
        printf("env list[%d]: %s\n", i, env[i]);
    }
    return 0;
}

配置文件

程序的配置文件格式也是五花八门:ini、xml、yml、json、properties、自定义语法(比如vim、nginx) 格式还分级别:系统默认级别<用户自定义级别<项目自定义级别,后面的优先级高于前面的优先级(比如Git、VSCode) 有些改了之后保存就能立马生效,有些需要重新加载页面(VSCode),有的则需要去重新启动程序(Idea、系统)

我这里主要想介绍下如何去监控一个文件的变化,来实现上面的功能;核心在于如何知道如何知道程序的配置文件变化了?写个定时器定期去查看文件内容是否变化了?老实说我一开始也是这么想的,后来找找,发现Linux系统内核有提供对应的系统调用syscall,https://man7.org/linux/man-pages/man7/inotify.7.html

文件行为列表:

  • IN_ACCESS (+) 文件被访问了 (e.g., read(2), execve(2)).
  • IN_ATTRIB (*) 文件元数据被改变了—for example, permissions (e.g., chmod(2)), timestamps (e.g., utimensat(2)), extended attributes (setxattr(2)), link count (since Linux 2.6.25; e.g., for the target of link(2) and for unlink(2)), and user/group ID (e.g., chown(2)).
  • IN_CLOSE_WRITE (+) 文件被打开,到写完close
  • IN_CLOSE_NOWRITE (*) File or directory not opened for writing was closed.
  • IN_CREATE (+) 文件或目录被创建了 (e.g., open(2) O_CREAT, mkdir(2), link(2), symlink(2), bind(2) on a UNIX domain socket).
  • IN_DELETE (+) 文件或目录被删除了
  • IN_DELETE_SELF Watched file/directory was itself deleted. (This event also occurs if an object is moved to another filesystem, since mv(1) in effect copies the file to the other filesystem and then deletes it from the original filesystem.) In addition, an IN_IGNORED event will subsequently be generated for the watch descriptor.
  • IN_MODIFY (+) 文件内容更新了 (e.g., write(2), truncate(2)).
  • IN_MOVE_SELF Watched file/directory was itself moved.
  • IN_MOVED_FROM (+) 文件被重命名了,旧的名字
  • IN_MOVED_TO (+) 文件被重命名了,新的名字
  • IN_OPEN (*) 文件被打开了

man手册的程序拷贝过来,就当我自己写了:poll监听,

c
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <string.h>

/* Read all available inotify events from the file descriptor 'fd'. wd is the table of watch descriptors for the
 * directories in argv. argc is the length of wd and argv. argv is the list of watched directories. Entry 0 of wd and
 * argv is unused. */

static void handle_events(int fd, int* wd, int argc, char* argv[]) {
    /* Some systems cannot read integer variables if they are not properly aligned. On other systems, incorrect
     * alignment may decrease performance. Hence, the buffer used for reading from the inotify file descriptor should
     * have the same alignment as struct inotify_event. */
    char                        buf[4096] __attribute__((aligned(__alignof__(struct inotify_event))));
    const struct inotify_event* event;
    ssize_t                     len;

    /* Loop while events can be read from inotify file descriptor. */
    for (;;) {
        /* Read some events. */
        len = read(fd, buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        /* If the nonblocking read() found no events to read, then it returns -1 with errno set to EAGAIN. In that case,
         * we exit the loop. */
        if (len <= 0) break;

        /* Loop over all events in the buffer. */
        for (char* ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len) {
            event = (const struct inotify_event*)ptr;

            /* Print event type. */
            if (event->mask & IN_OPEN) printf("IN_OPEN: ");
            if (event->mask & IN_CLOSE_NOWRITE) printf("IN_CLOSE_NOWRITE: ");
            if (event->mask & IN_CLOSE_WRITE) printf("IN_CLOSE_WRITE: ");

            /* Print the name of the watched directory. */
            for (int i = 1; i < argc; ++i) {
                if (wd[i] == event->wd) {
                    printf("%s/", argv[i]);
                    break;
                }
            }

            /* Print the name of the file. */
            if (event->len) printf("%s", event->name);
            /* Print type of filesystem object. */
            if (event->mask & IN_ISDIR)
                printf(" [directory]\n");
            else
                printf(" [file]\n");
        }
    }
}

int main(int argc, char* argv[]) {
    char          buf;
    int           fd, i, poll_num;
    int*          wd;
    nfds_t        nfds;
    struct pollfd fds[2];

    if (argc < 2) {
        printf("Usage: %s PATH [PATH ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    printf("Press ENTER key to terminate.\n");

    /* Create the file descriptor for accessing the inotify API. */
    fd = inotify_init1(IN_NONBLOCK);
    if (fd == -1) {
        perror("inotify_init1");
        exit(EXIT_FAILURE);
    }

    /* Allocate memory for watch descriptors. */
    wd = calloc(argc, sizeof(int));
    if (wd == NULL) {
        perror("calloc");
        exit(EXIT_FAILURE);
    }

    /* Mark directories for events - file was opened - file was closed */
    for (i = 1; i < argc; i++) {
        wd[i] = inotify_add_watch(fd, argv[i], IN_OPEN | IN_CLOSE);
        if (wd[i] == -1) {
            fprintf(stderr, "Cannot watch '%s': %s\n", argv[i], strerror(errno));
            exit(EXIT_FAILURE);
        }
    }

    /* Prepare for polling. */
    nfds = 2;

    fds[0].fd     = STDIN_FILENO; /* Console input */
    fds[0].events = POLLIN;

    fds[1].fd     = fd; /* Inotify input */
    fds[1].events = POLLIN;

    /* Wait for events and/or terminal input. */
    printf("Listening for events.\n");
    while (1) {
        poll_num = poll(fds, nfds, -1);
        if (poll_num == -1) {
            if (errno == EINTR) continue;
            perror("poll");
            exit(EXIT_FAILURE);
        }

        if (poll_num > 0) {
            if (fds[0].revents & POLLIN) {
                /* Console input is available. Empty stdin and quit. */
                while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n') continue;
                break;
            }

            if (fds[1].revents & POLLIN) {
                /* Inotify events are available. */
                handle_events(fd, wd, argc, argv);
            }
        }
    }

    printf("Listening for events stopped.\n");

    /* Close inotify file descriptor. */
    close(fd);
    free(wd);
    exit(EXIT_SUCCESS);
}

加载动态库

通常一个程序会去链接动态库,那程序在运行的时候是如何去加载这些额外的库文件?说明系统是有提供这种方式的。回到主题,如何去改变程序的行为呢?一种方式是我们可以通过更新动态库来升级程序,改变了代码的原有逻辑, 这就是所谓的热更新(不用重启程序)。另一种方式,我们也可以去加载额外的库,去调用库里面的函数

相应接口API:

c
#include <dlfcn.h>

void*       dlopen(const char* path, int mode);       // 打开一个动态链接库
void*       dlsym(void* handle, const char* symbol);  // 获取动态链接库中的符号(函数、变量)
int         dlclose(void* handle);                    // 关闭动态链接库
const char* dlerror(void);                            // 获取错误信息

示例:加载cjson库,输出一个json字符串

Mac平台的后缀是dylib,Linux平台的后缀是so,Windows平台后缀是dll,视自己使用平台而定

c


#include <dlfcn.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    void* handle = dlopen("libcjson.dylib", RTLD_LAZY);
    if (!handle) {
        printf("dlopen error: %s\n", dlerror());
        return -1;
    }
    void* (*cJSON_CreateObject)(void);
    void* (*cJSON_AddStringToObject)(void* const object, const char* const name, const char* const string);
    void* (*cJSON_AddNumberToObject)(void* const object, const char* const name, const double number);
    char* (*cJSON_PrintUnformatted)(const void* item);

    cJSON_CreateObject      = dlsym(handle, "cJSON_CreateObject");
    cJSON_AddStringToObject = dlsym(handle, "cJSON_AddStringToObject");
    cJSON_AddNumberToObject = dlsym(handle, "cJSON_AddNumberToObject");
    cJSON_PrintUnformatted  = dlsym(handle, "cJSON_PrintUnformatted");

    void* obj = cJSON_CreateObject();
    cJSON_AddStringToObject(obj, "name", "shibinbin");
    cJSON_AddNumberToObject(obj, "age", 18);
    char* json_str = cJSON_PrintUnformatted(obj);

    // 输出:json_str: {"name":"shibinbin","age":18}
    printf("json_str: %s\n", json_str);

    dlclose(handle);
    return 0;
}

Java的JNI也是上面这么实现的

CS模式,暴露接口

这种很常见,一般本机就用的Domain socket,跨主机就走Net Socket,及其它应用层协议Http、WebSocket、MQTT、RPC 比如本地的docker就同时暴露了Domain Socket:unix:///var/run/docker.sock和Net Socket:tcp://0.0.0.0:2375,当我们在终端上敲docker命令时,其实是一个创建了一个client和docker daemon通讯。

Updated at: