浅析 gowatch 监听文件变动实现原理

1,122 阅读3分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

刚开始接触go时,发现和解释型语言不同,go是编译型语言,即每次在有程序改动后,需要重新运行 go run或go build进行重新编译,更改才能生效,实则不便。于是乎在网络上搜索发现了gowatch这个包,该包可通过监听当前目录下相关文件的变动,对go文件实时编译,提高研发效率。那gowatch又是如何做到监听文件变化的呢?

通过阅读源码我们发现,在linux内核中,有一种用于通知用户空间程序文件系统变化的机制—Inotify。它监控文件系统,并且及时向专门的应用程序发出相关的事件警告,比如删除、读、写和卸载操作等。您还可以跟踪活动的源头和目标等细节。Golang的标准库syscall实现了该机制。为进一步扩展,实现了fsnotify包实现了一个基于通道的、跨平台的实时监听接口。如下图:

根据上图可知,监听文件的变化主要依赖于linux内核的INotify接口机制。Go的标准库中对其做了实现。而fsnotify package的主要作用就是将进一步封装成watcher结构体和事件类型结构体的封装,从而实现事件的判断以及目录的监听。下面看下 fsnotify package中对watcher的封装。

type Watcher struct {

    mu sync.Mutex // Map access

    fd int // File descriptor (as returned by the inotify_init() syscall)

    watches map[string]*watch // Map of inotify watches (key: path)

    fsnFlags map[string]uint32 // Map of watched files to flags used for filter

    fsnmut sync.Mutex // Protects access to fsnFlags.

    paths map[int]string // Map of watched paths (key: watch descriptor)

    Error chan error // Errors are sent on this channel

    internalEvent chan *FileEvent // Events are queued on this channel

    Event chan *FileEvent // Events are returned on this channel

    done chan bool // Channel for sending a "quit message" to the reader goroutine

    isClosed bool // Set to true when Close() is first called

}

linux内核Inotify接口简介

inotify中主要涉及3个接口。分别是inotify_init, inotify_add_watch,read。具体如下:

接口名作用
int fd = inotify_init()创建inotify实例,返回对应的文件描述符
inotify_add_watch (fd, path, mask)注册被监视目录或文件的事件
read (fd, buf, BUF_LEN)读取监听到的文件事件

Inotify可以监听的文件系统事件列表:

事件名称事件说明
IN_ACCESS文件被访问
IN_MODIFY文件被 write
IN_CLOSE_WRITE可写文件被 close
IN_OPEN文件被 open
IN_MOVED_TO文件被移来,如 mv、cp
IN_CREATE创建新文件
IN_DELETE文件被删除,如 rm
IN_DELETE_SELF自删除,即一个可执行文件在执行时删除自己
IN_MOVE_SELF自移动,即一个可执行文件在执行时移动自己
IN_ATTRIB文件属性被修改,如 chmod、chown、touch 等
IN_CLOSE_NOWRITE不可写文件被 close
IN_MOVED_FROM文件被移走,如 mv
IN_UNMOUNT宿主文件系统被 umount
IN_CLOSE文件被关闭,等同于(IN_CLOSE_WRITEIN_CLOSE_NOWRITE)
IN_MOVE文件被移动,等同于(IN_MOVED_FROMIN_MOVED_TO)

示例应用

接下来是一个简易的示例应用,具体的应用实例可参考github.com/silenceper/gowatch包源代码 。 主要逻辑如下:

  1. 初始化watcher对象
  2. 将文件或目录加入到watcher监控对象的队列
  3. 启动监听协程,实时获取文件对象事件
package main

import (

    "fmt"

    "github.com/howeyc/fsnotify"
    "runtime"
)


var exit chan bool

func main() {
    //1、初始化监控对象watcher
    watcher, err := fsnotify.NewWatcher() 

    if err != nil {

        fmt.Printf("Fail to create new Watcher[ %s ]\n", err)
    }

    //3、启动监听文件对象事件协程
    go func() {
        fmt.Println("开始监听文件变化")
        for {
            select {
            case e := <-watcher.Event:
                // 这里添加根据文件变化的业务逻辑
                fmt.Printf("监听到文件 - %s变化\n", e.Name)
                if e.IsCreate() {
                    fmt.Println("监听到文件创建事件")
                }
                if e.IsDelete() {
                    fmt.Println("监听到文件删除事件")
                }
                if e.IsModify() {
                    fmt.Println("监听到文件修改事件")
                }
                if e.IsRename() {
                    fmt.Println("监听到文件重命名事件")
                }
                if e.IsAttrib() {
                    fmt.Println("监听到文件属性修改事件")
                }

                fmt.Println("根据文件变化开始执行业务逻辑")
            
            case err := <-watcher.Error:

                fmt.Printf(" %s\n", err.Error())
            }
        }
    }()
    // 2、将需要监听的文件加入到watcher的监听队列中
    paths := []string{"config.yml"}

    for _, path := range paths {

        err = watcher.Watch(path) //将文件加入监听

        if err != nil {

            fmt.Sprintf("Fail to watch directory[ %s ]\n", err)
        }
    }

    <-exit
    runtime.Goexit()
}