fsnotify源码分析

简述

如果golang程序想监听文件系统的变化, 那么最普遍的做法是使用fsnotify库. 起初是由Chris Howey(github account: howeyc)开发的库, 后来受到广大开发者的喜爱, 遂单独建立仓库. 至今为止, 其仓库已收到了5.9k star, 这足以证明其受欢迎程度. 想了解更多关于fsnotify的历史, 可以查看官网.

源码分析

以下源码分析基于的git commit版本为: 7f4cf4dd2b522a984eaca51d1ccee54101d3414a

1. 代码统计

使用cloc工具进行源码统计, cloc --by-file-by-lang --exclude-dir=.github --exclude-lang=YAML,Markdown [project-dir], 结果如下(省略yaml等标记型语言相关统计):

File blank comment code
./integration_test.go 188 126 923
./inotify_test.go 69 28 358
./inotify_poller_test.go 29 10 190
./integration_darwin_test.go 31 31 105
./fsnotify_test.go 11 8 51
./windows.go 42 31 488
./kqueue.go 73 77 371
./inotify.go 45 66 226
./inotify_poller.go 16 33 138
./fsnotify.go 10 12 46
./fen.go 8 9 20
./open_mode_bsd.go 4 4 3
./open_mode_darwin.go 4 5 3
SUM: 530 440 2922

fsnotify的go代码总行数为2922行, 其中测试类代码占1627(=923+358+190+105+51)行, 实际有效代码只有1295行. 如此少的代码还支持了windows/linux/mac平台, 由此可见, 算是一个比较精简的库了.

2. 使用示例

为了先对代码有一个感性的认识, 我们以官方的示例作为开头, 代码如下:

package main

import (
    "log"

    "github.com/fsnotify/fsnotify"
)

func main() {
    watcher, err := fsnotify.NewWatcher() // 初始化一个空的watcher
    if err != nil {
        log.Fatal(err)
    }
    defer watcher.Close() // 最后结束程序时关闭watcher

    done := make(chan bool)
    go func() { // 启动一个协程来单独处理watcher发来的事件
        for {
            select {
            case event, ok := <-watcher.Events: // 正常的事件的处理逻辑
                if !ok {
                    return
                }
                log.Println("event:", event)
                if event.Op&fsnotify.Write == fsnotify.Write {
                    log.Println("modified file:", event.Name)
                }
            case err, ok := <-watcher.Errors: // 发生错误时的处理逻辑
                if !ok {
                    return
                }
                log.Println("error:", err)
            }
        }
    }()

    err = watcher.Add("/tmp/foo") // 使watcher监控/tmp/foo
    if err != nil {
        log.Fatal(err)
    }
    <-done // 使主协程不退出
}

用法非常的简单:

  1. 初始化一个空的fsnotify watcher
  2. 写一个协程用来处理watcher推送的事件
  3. 告诉watcher需要监听的文件或目录

3. 构建约束

fsnotify是一个跨平台的库, 源码中既包含了linux平台的实现逻辑, 也包含了mac平台和windows平台的实现逻辑, 此时问题就来了:

开发者在引用了此库后, 如何才能保证编译出来的可执行文件, 只包含对应的目标平台的实现, 而不包含无关平台的实现呢? 比如开发者的编译目标平台是linux, 编译时如何去除mac和windows等无关平台的实现代码呢?

好在golang为我们提供了构建约束(Build Constraints), 大概使用方法如下:

// +build linux,386 darwin,!cgo

上面这条注释不是普通的注释, 而是构建约束, 把它写在代码文件的顶部(package声明的上面), 会被编译器在编译时按照目标平台来判断是否编译进可执行文件中. 上面这行构建约束的意思是(linux AND 386) OR (darwin AND (NOT cgo)).

好了, 了解了构建约束的用法, 我们看fsnotify的源码时就可以根据自己所关心的平台来详细阅读其实现.

4. 详细解读--linux部分

用的最多的当属linux实现部分了, 其底层是基于linux的inotify机制, 相关逻辑就在库中的inotify.go文件中.

a. 总体思路

按照前面使用示例的步骤, 第一步是watcher, err := fsnotify.NewWatcher(), 那么我们就来看看这里new的watcher都包含什么, 代码如下:

// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
    // Create inotify fd
    fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC)
    if fd == -1 {
        return nil, errno
    }
    // Create epoll
    poller, err := newFdPoller(fd)
    if err != nil {
        unix.Close(fd)
        return nil, err
    }
    w := &Watcher{
        fd:       fd,
        poller:   poller,
        watches:  make(map[string]*watch),
        paths:    make(map[int]string),
        Events:   make(chan Event),
        Errors:   make(chan error),
        done:     make(chan struct{}),
        doneResp: make(chan struct{}),
    }

    go w.readEvents()
    return w, nil
}

上面代码的总体思路:

  1. 建立一个inotify实例

    inotify实例会以一个文件描述符(fd)的形式返回给调用者, 一旦有我们watch的文件发生变化, 就能从这个fd里读到相应的事件. 但是问题是这个文件描述符需要我们自己去读取, 所以我们就需要有某种轮训机制, 就引出下面的epoll注册的用处.

  2. 使用epoll监听实例上的事件

    把这个fd注册到epoll上, 在fd上有数据到达时, epoll就能立刻收到并返回给我们.

  3. 初始化各种的状态上下文, 如: watches用来存放watch对象, event用来推送事件

  4. 启动监听协程

b. 事件监听协程

上面的代码最后启动了一个监听协程go w.readEvents(), 我们就来看看这里发生了什么, 代码如下:

为使篇幅简练, 省略冗余代码

func (w *Watcher) readEvents() {
    var (...) // 各种变量
    defer close(...) // 关闭上下文的各种资源

    for {
        if w.isClosed() { return }

        ok, errno = w.poller.wait() // 程序阻塞在这行, 直到epoll监听到相关事件为止
        if ... { continue } // 各种error处理逻辑
    
        n, errno = unix.Read(w.fd, buf[:]) // 走到这里的话就是有事件发生, 所以这里读出事件到buffer里, 放到下面处理
        if ... { continue } // 各种error处理逻辑
        if n < unix.SizeofInotifyEvent { // 当读到的事件小于16字节(一个事件结构体的单位大小), 异常处理逻辑
            ...
            continue
        }

        var offset uint32
        // 此时我们也不知道读了几个事件到buffer里
        // 所以我们就用offset记录下当前所读到的位置偏移量, 直到读完为止
        // 这个for循环结束条件是: offset累加到了某个值, 以至于剩余字节数不够读取出一整个inotify event结构体
        for offset <= uint32(n-unix.SizeofInotifyEvent) {
            // 强制把地址值转换成inotify结构体
            raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))

            mask := uint32(raw.Mask) // 所发生的事件以掩码形式表示
            nameLen := uint32(raw.Len) // 当监听的是个目录时, 目录中发生事件的文件名会包含在结构体中, 这里的len就是文件名的长度

            if mask&unix.IN_Q_OVERFLOW != 0 { ... } // mask格式错误, 向Errors chan发送事件

            w.mu.Lock() // 由于可能会对上下文进行删除操作, 所以锁住
            // Wd是我们所watch的, 并且此次发生事件了的文件描述符
            // 我们可以从构建好的上下文中取出这个文件描述符所对应的文件名
            name, ok := w.paths[int(raw.Wd)] 
            // 如果发生删除事件, 也一并在上下文中删掉这个文件名
            if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
                delete(w.paths, int(raw.Wd))
                delete(w.watches, name)
            }
            w.mu.Unlock() // 解锁

            if nameLen > 0 { // 当我们watch是一个目录的时候, 其下面的文件发生事件时, 就会导致这个nameLen大于0
                // 此时读取文件名字(文件名就在inotify event结构体的后面), 强制把地址值转换成长度4096的byte数组
                bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
                // 拼接路径(文件名会以\000为结尾表示, 所以要去掉)
                name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
            }

            event := newEvent(name, mask) // 生成一个event
            if !event.ignoreLinux(mask) { // 如果这个事件没有被忽略, 那么发送到Events chan
                select {
                case w.Events <- event:
                case <-w.done:
                    return
                }
            }

            // 移动offset偏移量到下个inotify event结构体
            offset += unix.SizeofInotifyEvent + nameLen
        }
    }
}

c. 添加watch路径

我们通过err = watcher.Add("/tmp/foo")来让watcher去watch路径/tmp/foo, Add方法就是在inotify里注册路径, 代码如下:

// Add starts watching the named file or directory (non-recursively).
func (w *Watcher) Add(name string) error {
    name = filepath.Clean(name) // 获取标准路径, 如/tmp//////too经过Clean后就成了/tmp/too
    if w.isClosed() {
        return errors.New("inotify instance already closed")
    }

    const agnosticEvents = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
        unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
        unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF

    var flags uint32 = agnosticEvents

    w.mu.Lock()
    defer w.mu.Unlock()
  watchEntry := w.watches[name] // 取出上下文里的watch路径(如果存在的话)
    if watchEntry != nil {
        flags |= watchEntry.flags | unix.IN_MASK_ADD
    }
    wd, errno := unix.InotifyAddWatch(w.fd, name, flags) // 添加watch路径
    if wd == -1 {
        return errno
    }

    if watchEntry == nil { // 如果上下文里不存在此路径, 表明这是一个新的watch, 添加到上下文
        w.watches[name] = &watch{wd: uint32(wd), flags: flags}
        w.paths[wd] = name
    } else { // 如果在上下文中存在, 则更新上下文
        watchEntry.wd = uint32(wd)
        watchEntry.flags = flags
    }

    return nil
}

d. 删除watch路径

// Remove stops watching the named file or directory (non-recursively).
func (w *Watcher) Remove(name string) error {
    name = filepath.Clean(name) // 获取标准路径

    // 涉及到多协程可能同时对同一个watch项写, 所以锁住
    w.mu.Lock()
    defer w.mu.Unlock() // 最后解锁
    watch, ok := w.watches[name]

    if ... { ... } // 错误处理

    // 删除上下文里的相应watch项
    delete(w.paths, int(watch.wd))
    delete(w.watches, name)
    // 删除inotify中watch的fd
    success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
    if ... { ... } // 错误处理
  
    return nil
}

e. poller部分(基于epoll)

我们上面看到在func NewWatcher() (*Watcher, error)函数中调用了poller, err := newFdPoller(fd), 这是将inotify的fd注册在epoll上, 以实现高效的fs监听, 代码如下:

为使篇幅简练, 省略冗余代码

func newFdPoller(fd int) (*fdPoller, error) {
    var errno error
    poller := emptyPoller(fd)
    defer func() {
        if errno != nil {
            poller.close()
        }
    }()
    poller.fd = fd

    // 要使用epoll的话, 需要使用EpollCreate函数为其单独创建一个fd
    poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC)
    if ... { return ... } // error处理
  
    // 为实现优雅退出, 需要创建一个管道, pipe[0]用来读, pipe[1]用来写
  // 在介绍watcher的Close函数时会分析这部分的逻辑
    errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC)
    if ... { return ... } // error处理

    // 注册inotify的fd到epoll
    event := unix.EpollEvent{
        Fd:     int32(poller.fd),
        Events: unix.EPOLLIN,
    }
    errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event)
    if ... { return ... } // error处理

    // 注册管道的fd到epoll
    event = unix.EpollEvent{
        Fd:     int32(poller.pipe[0]),
        Events: unix.EPOLLIN,
    }
    errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event)
    if ... { return ... } // error处理

    return poller, nil
}

函数func newFdPoller(fd int) (*fdPoller, error)在epoll的fd上注册了两个文件, 一个是inotify的, 另一个是其用来实现优雅退出的pipe[0].

我们在上面的*事件监听协程 func (w Watcher) readEvents()小节中提到的ok, errno = w.poller.wait()语句阻塞直到收到事件才会返回, 来看看具体poller(也就是上面的epoll)对事件的处理逻辑, 代码如下:

为使篇幅简练, 省略冗余代码

func (poller *fdPoller) wait() (bool, error) {
    // 总共监听两个fd: 1.inotify 2.优雅退出所需的pipe[0]
    // 每个fd有三种可能的事件, 所以最多可以触发6个事件
    // 准备一个大于6的slice
    events := make([]unix.EpollEvent, 7)
    for {
        // 阻塞wait在epoll的fd上, 参数-1表示不会超时
        // 一旦有事件产生, 就会发到events里
        n, errno := unix.EpollWait(poller.epfd, events, -1) 
        if ... { ... } // 各种异常处理

        // 以下就是收到正常事件的处理
        ready := events[:n]
        epollhup := false
        epollerr := false
        epollin := false
        for _, event := range ready {
            if event.Fd == int32(poller.fd) {
                if event.Events&unix.EPOLLHUP != 0 {
                    // This should not happen, but if it does, treat it as a wakeup.
                    epollhup = true
                }
                if event.Events&unix.EPOLLERR != 0 {
                    // If an error is waiting on the file descriptor, we should pretend
                    // something is ready to read, and let unix.Read pick up the error.
                    epollerr = true
                }
                if event.Events&unix.EPOLLIN != 0 {
                    // inotify有事件
                    epollin = true
                }
            }
            if event.Fd == int32(poller.pipe[0]) {
                if event.Events&unix.EPOLLHUP != 0 {
                    // Write pipe descriptor was closed, by us. This means we're closing down the
                    // watcher, and we should wake up.
                }
                if event.Events&unix.EPOLLERR != 0 {
                    // If an error is waiting on the pipe file descriptor.
                    // This is an absolute mystery, and should never ever happen.
                    return false, errors.New("Error on the pipe descriptor.")
                }
                if event.Events&unix.EPOLLIN != 0 {
                    // 收到程序发来的优雅退出事件, 将调用clearWake以使管道排空
                    err := poller.clearWake()
                    if err != nil {
                        return false, err
                    }
                }
            }
        }

        if epollhup || epollerr || epollin {
            return true, nil
        }
        return false, nil
    }
}

clearWake函数, 代码如下

func (poller *fdPoller) clearWake() error {
   // You have to be woken up a LOT in order to get to 100!
   buf := make([]byte, 100)
   n, errno := unix.Read(poller.pipe[0], buf) // 读取pipe[0]中的退出信号
   if ... { ... } // 错误处理
   return nil
}

那么pipe[0]中的信号是怎么来的呢? 也就是说必须有一个地方往pipe[1]中写数据. 其实, 我们示例代码中采用defer方式调用了watcher.Close()函数, 而其最重要的一步就是调用w.poller.wake()函数, 代码如下:

为使篇幅简练, 省略冗余代码

// Close the write end of the poller.
func (poller *fdPoller) wake() error {
    buf := make([]byte, 1)
    // 这里在pipe[1]写入了一个字符当做退出信号
    n, errno := unix.Write(poller.pipe[1], buf)
    if ... { ... } // 错误处理
    return nil
}

题外话: 关于这个优雅退出的早期设计其实不是这样的, 但是思路差不多. 有兴趣可以去看看fsnotify的早期提交

至此, 关于fsnotify对linux的实现就分析完了.

你可能感兴趣的:(fsnotify源码分析)