大家好,我是神棍局副局长、小范围著名的谢顶道人 --- 老李。
作为众多打工人中的一员,老李每天早上醒来都是奄奄一息的,那么,怎么着才能打满鸡血变成元气满满的一天呢?当然是拍手舞了,那么拍手舞怎么跳呢?贴心老李自然还要再送你一个在线拍手舞教程:
最近总是看到有泥腿子在抱怨啊,说什么「面试造火箭,进厂拧螺丝」,要么就是「一天天复制粘贴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具备如下几个优点:
而它的API就三个,非常粗暴,你们感受一下:
简单说明一下inotify API的使用流程:
只有三个API,想必代码一定很好写了(不看注释,损失三个亿,加上你在厕所已经损失的那三个亿,一共六亿):
#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:
<?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