tail命令是最常用来看日志改变的工具,比如在执行某个任务时会往本地文件中打入日志,然后使用类似
tail -f file_path
的命令来查看最新的日志信息。
要实现这种功能,一般会想到轮询,也就是不断地去读取文件然后比较内容,输出最新的即可,网上搜一下也是有不少这种实现(比如这个)。
显然这不够优雅。
之前听河狸家的技术总监就说到了这个的解决方案,查了一下,发现使用inotify来监听文件变化并向程序发送事件,再用select,poll,epoll来监听inotify产生的事件可以完成tail命令的基本功能。
为此,了解一下相关的调用。
inotify相关
头文件
#include<sys/inotify.h>
初始化
int fd = inotify_init();
此处的fd可以理解为inotify创建的一个通知事件的文件描述符,类似网络编程中的socket,当监听文件变化时,inotify会向fd中写入事件的相关信息。
添加监听
int wd = inotify_add_watch(int fd, const char *path, unit32_t mask);
这里的fd就是初始化调用inotify_init返回的文件描述符,path就是你要监听的文件路径,mask就是你要监听的变化类型,可以有很多种组合,返回值wd在文档上就这么一句话:
On success, inotify_add_watch() returns a nonnegative watch descriptor. On error -1 is returned and errno is set appropriately.
让人摸不着头脑,不知道返回的到底是哪个文件的描述符(事实证明这个wd也不是path文件的描述符)。
mask 可以是以下值的组合
- IN_ACCESS,文件被访问
- IN_ATTRIB,文件属性被修改
- IN_CLOSE_WRITE,可写文件被关闭
- IN_CLOSE_NOWRITE,不可写文件被关闭
- IN_CREATE,文件/文件夹被创建
- IN_DELETE,文件/文件夹被删除
- IN_DELETE_SELF,被监控的对象本身被删除
- IN_MODIFY,文件被修改
- IN_MOVE_SELF,被监控的对象本身被移动
- IN_MOVED_FROM,文件被移出被监控目录
- IN_MOVED_TO,文件被移入被监控目录
- IN_OPEN,文件被打开
实现tail是用的IN_MODIFY
当调用添加监视对象后就可以坐等事件发生了,当path对应的文件被修改后,fd就变得可读,而且变化的消息也写入了fd,这里我们就可以用select,poll,epoll来监听fd以变相监听文件的变化了。
epoll相关
创建
int epfd = epoll_create(int size);
创建一个epoll,size是要监听的文件数量
注册
int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event)
epfd就是创建epoll_create返回值,op表示动作,
可选的有
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd;
fd是要监听的文件
event 是需要监听的内容,例如fd可读,可写等等。
等待事件发生
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
前两个参数都是上面的,第三个参数maxevents其实是告诉内核 events 有多大,不会超过创建时的size,第四个参数timeout是指等待的超时时间,比如设为500,表示500毫秒不管有没有事件发生都会返回,设为0则一直阻塞直到事件发生。返回值是发生事件的个数。
介绍完几个系统调用,就可以开始撸代码了。花了几个小时撸了一个简易版本,好在可以用,只限于append。
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/inotify.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#define BUFSIZE 81
int main(int argc, char* argv[]) {
printf("LOG:argc = %d\n", argc);
printf("LOG:argc[1] = %s\n", argv[1]);
int fd = inotify_init();
printf("LOG:fd = %d\n", fd);
if (fd < 0) {
printf("LOG:inotify_init error\n");
return -1;
}
int wd = inotify_add_watch(fd, argv[1], IN_MODIFY);
printf("LOG:wd = %d\n", wd);
if (wd < 0) {
printf("LOG:inotify_add_watch error");
return -1;
}
int epfd = epoll_create(1);
printf("LOG:epfd = %d\n", epfd);
if (epfd < 0) {
printf("LOG:epoll_create error\n");
return -1;
}
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
event.data.fd = fd;
int re = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
printf("LOG:re = %d\n", re);
if (re != 0) {
printf("LOG:epoll_ctl error:%s\n", strerror(errno));
return -1;
}
ssize_t n;
ssize_t tmp;
int file_fd = open(argv[1], O_RDONLY);
printf("LOG:file_fd = %d\n", file_fd);
if (file_fd == -1) {
printf("LOG:open error\n");
return -1;
}
char buf[BUFSIZE];
int seek_re;
while (1) {
int num = epoll_wait(epfd, &event, 1, -1);
printf("LOG:epoll_wait num = %d\n", num);
if (num > 0) {
// 这里是为了读掉消息而不重复通知
tmp = read(fd, buf, BUFSIZE);
seek_re = lseek(file_fd, (off_t) -1, SEEK_CUR);
n = read(file_fd, buf, BUFSIZE);
if (n == -1) {
printf("break\n");
break;
}
if (n > 0) {
printf("%s", buf);
}
}
printf("LOG:new loop\n");
}
return 0;
}
编译
gcc mytail.c -o mytail
执行
touch ./test.txt
./mytail ./test.txt
另开一个终端
echo aa > ./test.txt
echo aabb > ./test.txt
echo aabbcc > ./test.txt
可以看到类似tail的输出,不过多了一些log。
Q&A
Q:最后为啥我用vim编辑test.txt文件再保存却没有效果呢?
A:其实是这样的,vim这类编辑器并没有修改文件,而是copy一份来编辑,编辑完了替换掉原先的文件,如果要实现这个,可以用notify监听文件的删除移动等。
Q:为啥是epoll而不是select,poll?
A:这个纯属好玩,用select,poll也能达到相同的效果,在监听少数文件下是没有区别的,网上说的差别主要还是针对监听大量文件的情况下,通常也是网络请求高并发下,epoll会突显绝对优势,这里epoll只监听inotify的fd文件,永远只有一个,所以更没有区别了。
Q:既然epoll可以监听inotify的事件,为何epoll不直接监听变化的文件而是要绕这么大一个弯?
A:linux中的epoll本身不支持监听本地文件,只能监听类似inotify和socket这种文件(可以理解为消息管道,并不存储信息),当有事件到达这两种文件,这两种文件被写入了信息,并且变得可读,本地文件写入了新东西并没有变得可读。