前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >来,老李带你整点儿不一样的(一)

来,老李带你整点儿不一样的(一)

作者头像
老李秀
发布2020-11-09 14:23:59
9080
发布2020-11-09 14:23:59
举报
文章被收录于专栏:可能是东半球最正规的API社区

大家好,我是神棍局副局长、小范围著名的谢顶道人 --- 老李。

作为众多打工人中的一员,老李每天早上醒来都是奄奄一息的,那么,怎么着才能打满鸡血变成元气满满的一天呢?当然是拍手舞了,那么拍手舞怎么跳呢?贴心老李自然还要再送你一个在线拍手舞教程:

最近总是看到有泥腿子在抱怨啊,说什么「面试造火箭,进厂拧螺丝」,要么就是「一天天复制粘贴CURD」,这是两个核心问题:

  • 一天天CURD真没劲,感觉是塑料程序员、32K真码农
  • 为毛面试都是长征火箭级别的,真能进去后还是复制粘贴CURD

「过度CURD以后,腰腿酸痛,精神不振,感觉身体被掏空。是不是被透支了?怕再也不能给CURD稳稳的幸福... ...」

「他来了!他来了!他脚踏祥云过来了!今天,油腻老李,谢顶道人,在线解惑!三十年老军医,专治不...」

「...你好,CURD也好」

谢顶道人将通过一个小系列带各路神棍、泥腿子体验一把飞一般的感觉:LogAgent蛋生记。先不说LogAgent是干啥的,今天先入门我先提出一个技术场景问题:

如何高效地监控文件发生变动?

高不高效不知道,反正各路神棍们不约而同地说:「先打开文件,然后while true不断地怼就是了,就跟打桩机似的、就跟电动小马达似的,不停...」,一小部分泥腿子思路或者感觉大概是有的,说「类似于IO多路复用那种方式」,不过这一小部分泥腿子很快、迅速地就被大部分坚持「能用就行」的泥腿子给干翻剿灭了:

(截图来自于:《疯狂的石头》)

实际上,早很很久之前的公元2005年,当Linux Kernel 2.6.13发布的时候,文件系统中就集成了一个叫做inotify的组件,这个玩意的作者分别是John、Amy和Robot(排名不分先后)。inotify出现的目的是为了代替Linux Kernel中的dnotify(由于历史比较久远老李本身也没有碰过dnotify)。据史料记载这个inotify具备如下几个优点:

  • 避免了while true这种人肉打桩机、电动小马达形式的主动轮训方案
  • 避免像dnotify那种「被监控文件或者目录」不得不都要创建fd的浪费
  • 提升监控细粒度,除了目录外,还可以监控具体文件,而据史料记载dnotify只能监控目录(注意是据史料记载)
  • inotify还可以直接利用select、poll这种IO多路复用,非常方便

而它的API就三个,非常粗暴,你们感受一下:

  • int inotify_init(void)
  • int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
  • int inotify_rm_watch(int fd, int wd);

简单说明一下inotify API的使用流程:

  • 第一步利用inotify_init()初始化返回一个fd
  • 然后将该fd与「要监控的文件或者目录路径」与「要监控的文件改变类型」三个参数一同放到inotify_add_watch()中,该函数返回一个wd(请理解为watch描述符)
  • 最后一步并不是inotify_rm_watch()而是利用read()读取事件流然后对事件进行判断分析即可,其实就是一大坨if else针对不同的改变类型做出不同的响应来。inotify会将文件/文件夹上发生的事件(本质上是个inotify_event结构体)按照顺序存储起来,利用read()读取第一步里的文件描述,事件会被存储到read()的函数的buffer中去

只有三个API,想必代码一定很好写了(不看注释,损失三个亿,加上你在厕所已经损失的那三个亿,一共六亿):

代码语言:javascript
复制
#include <stdio.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <strings.h>
#define BUF_SIZE 100000

int main(int argc, char **argv) {
    int inotify_fd;
    int inotify_watch_fd;
    int read_buf_length;
    char * file_path = "./api.log";
    char buffer[BUF_SIZE];
    char * p;
    struct inotify_event * single_inotify_event;
    // 第一步:init一下咯...bzero()是为了清空一下内存,保证无脏数据
    bzero(buffer, BUF_SIZE);
    inotify_fd = inotify_init();
    if (-1 == inotify_fd) {
        printf("inotify-init-error");
        return -1;
    }
    // 第二步:添加watch,将要监控的文件或者目录搞进来,最后一个参数是要监控的事件类型
    /*
       注意最后一个参数,是一个mask。他有如下几个数值:
       IN_OPEN 就是打开文件被监控到了
       IN_CLOSE_WRITE 打开文件、写文件、关闭
       IN_CLOSE_NOWRITE  打开文件,看了看,啥也没干关闭了
       IN_MODIFY   文件被改动了
       IN_DELETE  被unlink删除了...
       太多了,所有情况看下面程序demo
    */
    inotify_watch_fd = inotify_add_watch(inotify_fd, file_path, IN_ALL_EVENTS);
    if (-1 == inotify_watch_fd) {
        printf("inotify-watch-error");
        return -1;
    }
    // 第三步:loop起来。有人会说:你这个loop不也是打桩机吗?
    // 但是这个打桩机至少是半自动打桩机。它只有文件状态真的发生变化时候才会打桩
    // 一句话:老打桩机打桩是为了发现文件状态变化,新打桩机是发了变化后才会打桩
    while(1) {
        //printf("in while-loop pre\r\n");
        // 默认情况下,read会被阻塞起来,一直到从inotify-fd中读取到变化并存储d到buffer中去
        // 读取到的内容:就是下面event结构体,可能会有好几个
        /*
         struct inotify_event {
               int      wd;       // watch文件描述符
               uint32_t mask;    // 所发生变化的mask数值
               uint32_t cookie;  // 据文档说当下只有监控目录,发生move-from和move-to的时候,用于串联事件使用,其他概况一般默认都是0           
               uint32_t len;     // ?name的长度
               char     name[];  // 只有监控目录变化时候,目录中新增文件等name就会有数值了
         };
        */
        read_buf_length = read(inotify_fd, buffer, BUF_SIZE);
        //printf("in while-loop | read_length = %d\r\n", read_buf_length);
        if (-1 == read_buf_length) {
            printf("read-error");
            continue;
        }
        for (p = buffer; p < buffer+read_buf_length;) {
            single_inotify_event = (struct inotify_event *)p;

            printf("wd = %2d, name=%s\r\n", single_inotify_event->wd, single_inotify_event->name);
            /*
             截止到目前为止,还有很多人不明白这种用mask实现类似于开关或者
             配置的好处和优势,包括原理...
             */
            if (single_inotify_event->mask & IN_ACCESS) {
                printf("in-access\r\n");
            }
            if (single_inotify_event->mask & IN_ATTRIB) {
                printf("in-attrib\r\n");
            }
            if (single_inotify_event->mask & IN_CREATE) {
                printf("in-create\r\n");
            }
            if (single_inotify_event->mask & IN_MODIFY) {
                printf("in-modify\r\n");
            }
            if (single_inotify_event->mask & IN_OPEN) {
                printf("in-open\r\n");
            }
            if (single_inotify_event->mask & IN_CLOSE_WRITE) {
                printf("in-close-write\r\n");
            }
            if (single_inotify_event->mask & IN_CLOSE_NOWRITE) {
                printf("in-close-nowrite\r\n");
            }
            if (single_inotify_event->mask & IN_MOVE_SELF) {
                printf("in-move-self\r\n");
            }
            if (single_inotify_event->mask & IN_MOVED_FROM) {
                printf("in-moved-from\r\n");
            }
            if (single_inotify_event->mask & IN_MOVED_TO) {
                printf("in-move-to\r\n");
            }
            if (single_inotify_event->mask & IN_IGNORED) {
                printf("in-IGNORED\r\n");
            }
            if (single_inotify_event->mask & IN_DELETE) {
                printf("in-delete\r\n");
            }
            if (single_inotify_event->mask & IN_DELETE_SELF) {
                printf("in-delete-self\r\n");
            }
            /*
            下面这行有个难点需要注意:就是buffer中这一连串的inotify_event,读第一个后,如何读出第二个?
            界定一个inotify_event结构体的长度是:sizeof(struct inotify_event) + single_inotify_event->len
            不要忘记了inotify_event结构体中有一个成员是name,是char[],他的长度用另一个成员len可以获取到
            所以,这就是在buffer中界定一个inotfiy_event边界的办法
            能界定边界了,程序就可以读完buffer中所有的event了
            
            那么问题又来了,这个buffer应该定成多长呢?因为name长度是不定长的,所以有一种鸡贼的办法:
            (sizeof(struct inotify_event) + NAME_MAX + 1) * 事件数量(最多十来个)
            结构体本身c长度 加上 NAME_MAX常量(表示文件名最大长度),再加上1是末尾的'\0'长度
            由于一个文件上所能发生的inotify事件数量是有上限的,所以不要手软,直接写s上限最大数值
            所以这样的buffer,是一定足够盛放所有event结构体了        
             */
            p += (sizeof(struct inotify_event) + single_inotify_event->len);
        }
        printf("--- one-loop-end ---\r\n\r\n\r\n");
    }
}

gcc搞一下跑个测试吧,注意记得同级别目录下创建好api.log文本文件。大概如下图所示,你们感受一下:

我建议大伙儿把所有事件都尝试一下,上面是对某个文件的监控,你一定要再试试文件夹的。除此之外提醒一点儿:可能会有人用vim对api.log进行编辑,但是除了vim打开文件时候能看到效果,你写内容都不会看到效果,原因是啥?自己思考思考。

那事情到这儿就有泥腿子要问了:你这个用C写的demo,直接对接Linux API,我就一个PHP泥腿子,连Go也不会,我能咋办?不,腿子,听我说,PHP也可以办。心有多宽广,舞台就有多大!只要你想干!PHP都能写LogAgent!

首先下载并安装PHP版本的inotify扩展(我假装你们都会能搞定),然后复制粘贴下面的demo:

代码语言:javascript
复制
<?php
$s_file       = "./api.log";
$i_inotify_fd = inotify_init();
$i_watch_fd   = inotify_add_watch($i_inotify_fd, $s_file, IN_ALL_EVENTS);

// 将$i_inotify_fd设置为非阻塞IO
// 看过《PHP网络编程》的朋友,不应该对“ 阻塞和非阻塞 ”这个概念这个陌生了
// 当然了,下面这样你可以完全注释掉
// 注释掉:inotify_read就阻塞一直等待有事件发生
// 不注释:inotify_read不会阻塞,会一直打空炮
stream_set_blocking($i_inotify_fd, 0);

while (true) {
    $a_events = inotify_read($i_inotify_fd);
    //sleep(1);
    //print_r($a_events);
    $i_event_length = inotify_queue_len($i_inotify_fd);
    echo $i_event_length.PHP_EOL;
}

demo代码,跑跑试试看???

所以,你们知道Linux下tail -f命令的原理了吗?如果说你的工作中除了正常CURD外,还需要你写一个LogAgent或者结合自家业务二次开发一个LogAgent,那么你有思路吗?相对于天天CURD,做基础件是不是很爽呢?后面章节里,我们将逐步利用inotify实现一个LogAgent。

你以为会用inotify就很装逼了,实际上这是第一层,因为往下还有inotify的实现原理...

如果想继续深造的神棍们,我给大家推荐一本书:《Linux/UNIX系统编程手册》,这书是神棍局图书馆必备。其实这书你可以理解为man7大集合,涵盖了所有Linux系统编程的API,相对于APUE来说,这本书算是辞典。

资料:

https://man7.org/linux/man-pages/man7/inotify.7.html

https://man7.org/linux/man-pages/man2/inotify_init.2.html

https://man7.org/linux/man-pages/man2/inotify_add_watch.2.html

http://doc.p2hp.com/function.inotify-init.html

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能API社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档