【问题标题】:data structure for file path string find insert delete [closed]文件路径字符串的数据结构查找插入删除[关闭]
【发布时间】:2025-12-31 21:15:02
【问题描述】:

我监控文件系统事件,并使用路径作为键存储事件,当路径的多个事件发生时,我需要找到前一个事件并将它们合并。

根据路径存储和查找文件系统事件的数据结构和算法。

【问题讨论】:

    标签: c linux algorithm data-structures


    【解决方案1】:

    我会使用一个结构来描述每个事件,包括作为 C99 灵活数组成员的路径:

    struct event {
        /* Event queue pointers */
        struct event *queue_prev;
        struct event *queue_next;
    
        /* Event identifying bit mask */
        size_t        mask;
    
        /* Path hash table */
        struct event *hash_next;
        size_t        hash;
        unsigned char path[];
    };
    

    事件队列本身是一个双向链表,以避免在根据路径删除事件时必须遍历整个链表。

    每个哈希表槽都是一个单链事件列表。这是因为我们希望这些链很短,否则哈希表效率不高。

    我不确定哪种哈希函数效果最好,但我会从好的旧 DJB2 开始:

    size_t djb2(const unsigned char *src)
    {
        size_t  hash = 5381;
        while (*src)
            hash = ((hash << 5) + hash) ^ (*(src++));
        return hash;
    }
    

    事件队列和路径哈希表将使用

    来描述
    struct event_queue {
        /* Event queue */
        size_t        events;
        struct event *first;  /* First event in queue */
        struct event *last;   /* Last event in queue */
        /* Path hash table */
        size_t        slots;
        struct event *slot;
    };
    

    由于每个路径哈希表槽的成本只是一个指针,因此最好从一个相对较大的哈希表开始,比如 256 个槽。这不是限制;它只是变得更慢(因为需要遍历插槽链)。它可能应该是一个可配置的设置。要初始化事件队列,我会使用

    int event_queue_init(struct event_queue *queue, size_t slots)
    {
        if (!queue || slots < 1)
            return errno = EINVAL;
    
        /* The queue itself is empty. */
        queue->events = 0;
        queue->first  = NULL;
        queue->last   = NULL;
    
        queue->slots  = slots;
        queue->slot   = calloc(slots, sizeof queue->slot[0]);
        /* Note: In Linux, queue->slot is now full of NULL pointers.
                 This may not be true on other OSes! */
    
        if (!queue->slot)
            return errno = ENOMEM;
    }
    

    在运行时调整哈希表的大小可能是个好主意,即使这不是绝对必要的:

    int event_queue_resize(struct event_queue *queue, size_t slots)
    {
        size_t i, n;
    
        if (!queue || slots < 1)
            return errno = EINVAL;
    
        struct event *slot[] = calloc(slots, sizeof slot[0]);
        if (!slot)
            return errno = ENOMEM;
        /* Note: In Linux, slot is now full of NULL pointers.
                 This may not be true on all OSes! */
    
        n = 0;
        i = queue->slots;
        while (i-->0) {
            struct event *next = queue->slot[i];
            queue->slot[i] = NULL;
            while (next) {
                struct event *curr = next;
                size_t        slotnum = curr->hash % slots;
    
                next = next->hash_next;
                n++;
    
                curr->hash_next = slot[slotnum];
                slot[slotnum] = curr;
            }
        }
        free(queue->slot);
        queue->slots  = slots;
        queue->slot   = slot;
        queue->events = n;
    }
    

    即使有排队的事件,调整哈希表的大小也是安全的,因为事件队列指针不受哈希表调整大小的影响。

    要将事件附加到队列中,如果已排队,则修改相同路径的现有事件:

    int event_append(struct event_queue *queue,
                     const unsigned char *path,
                     size_t mask)
    {
        if (!queue || !path)
            return errno = EINVAL;
    
        const size_t  hash = djb2(path);
        const size_t  slotnum = hash % queue->slots;
    
        struct event *ev = queue->slot[slotnum];
        while (ev) {
            if (ev->hash == hash && !strcmp(ev->path, path)) {
    
                /* Event already queued. */
                ev->mask |= mask;
    
                return 0;
            } else {
                ev = ev->hash_next;
            }
        }
    
        const size_t pathlen = strlen(path);
    
        ev = malloc(sizeof (struct event) + pathlen + 1);
        if (!ev)
            return errno = ENOMEM;
    
        memcpy(ev->path, path, pathlen + 1); /* Include '\0' at end */
        ev->hash = hash;
        ev->mask = mask;
    
        /* Prepend to hash table */
        ev->hash_next = queue->slot[slotnum];
        queue->slot[slotnum] = ev;
    
        /* Append to queue */
        if (!queue->first) {
            ev->queue_prev = NULL;
            ev->queue_next = NULL;
            queue->first = ev;
            queue->last = ev;
            queue->events = 1;
        } else {
            ev->queue_prev = queue->last;
            ev->queue_next = NULL;
            queue->last->queue_next = ev;
            queue->last = ev;
            queue->events++;
        }
    
        return 0;
    }
    

    弹出队列中的下一个事件:

    struct event *event_next(struct event_queue *queue)
    {
        if (!queue)
            return NULL;
    
        struct event *ev = queue->first;
        if (!ev)
            return NULL;
    
        queue->first = ev->queue_next;
        if (!queue->first) {
            queue->last = NULL;
            queue->events = 0;
        } else {
            queue->first->queue_prev = NULL;
        }
    
        ev->queue_prev = NULL;
        ev->queue_next = NULL;
    
        queue->events--;
    
        /* Remove from the hash table */
        struct event **ptr = queue->slot + ev->hash % queue->slots;
        struct event *evt = *ptr;
        while (evt) {
            if (evt == ev) {
                *ptr = ev->hash_next;
                break;
            } else {
                ptr = &(evt->hash_next);
                evt = *ptr;
            }
        }
    
        return ev;
    }
    

    取消特定路径上的事件:

    struct event *event_dequeue(struct event_queue *queue,
                                const unsigned char *path)
    {
        if (!queue || !path)
            return NULL;
    
        const size_t   hash = djb2(path);
        const size_t   slotnum = hash % queue->slots;
        struct event **ptr = queue->slot + slotnum;
        struct event  *ev = *ptr;
    
        while (ev) {
            if (ev->hash == hash && !strcmp(ev->path, path)) {
                /* Remove from event queue */
                if (ev->queue_prev)
                    ev->queue_prev->queue_next = ev->queue_next;
                if (ev->queue_next)
                    ev->queue_next->queue_prev = ev->queue_prev;
                if (queue->first == ev)
                    queue->first = ev->queue_next;
                if (queue->last == ev)
                    queue->last = ev->queue_prev;
                queue->events--;
                /* Remove from hash table slot */
                *ptr = ev->next;
                /* Done. */
                return ev;
            }
    
            ptr = &(ev->next);
            ev = *ptr;
        }
    
        return NULL;
    }
    

    这些只是向您展示想法的即兴实现;我相信你可以更好地实现它们。

    请注意,您可以仅使用 queue_prevqueue_next 指针在队列中向上/向下移动事件,但您还需要更新队列 @9​​87654331@ 和 last 指针,如果你改变任何一个。

    对于线程安全操作,我会在操作期间持有的struct event_queue 中使用pthread_mutex_t。每个操作(哈希表调整大小除外)花费的时间太短,以至于增加的代码复杂度允许并发访问是不值得的。

    【讨论】:

    • 我测试了这个用于gnu elf符号表的哈希函数h = h * 33 + c,它在我的计算机540 thounds路径上哈希非常平衡,没有相同的哈希结果。
    • @yuanjianpeng:是的,就是同一个 DJB 哈希函数。 (有两种变体,一种使用'+',另一种使用'^';(x &lt;&lt; 5) + x == 33*x 表示任何无符号整数类型的xsize_t 是无符号整数类型。)它确实适用于人类-根据我的经验,可读文本,但由于我没有将它与其他散列函数进行比较,我不想声称它比其他散列好/更好。