1、nn_cont
这个宏是嵌入式、linux内核中最经典、最核心的宏之一,官方标准名叫container_of,Nanomsg中改名叫nn_cont,其功能完全一样。它的作用是,**通过结构体中某个成员变量的指针,反推出整个结构体变量的首地址。**其代码如下:
c
#define nn_cont(ptr, type, member) \
(ptr ? ((type*) (((char*) ptr) - offsetof(type, member))) : NULL)
// 定义一个结构体
struct student {
int id; // 成员1
char name[10]; // 成员2
int age; // 成员3 <-- 我们就用这个成员
};
struct student stu;
stu.age = 18;
// 我们只有 age 的指针
int *p_age = &stu.age;
// 如上面student的结构体,只有age的指针,怎么能stu的指针呢?
// 通过 age 指针就可以得到整个 student 结构体指针
struct student *p_stu = nn_cont(p_age, struct student, age);
其中offsetof(type, member)是标准库宏,其作用就是计算一个成员在结构体中的字节偏移量,offsetof(struct student, age) 结果就是 14。
2、efd(Nanomsg 通过 efd 把内部事件统一成 "可 poll 的 fd")
2.1、eventfd
eventfd 是 Linux 特有的内核事件通知机制,其本质是内核维护一个uint64_t类型计数器,将其对外映射成标准文件描述符 fd,这样做的目的是适配 Linux 统一 IO 接口,能用 poll/epoll/read/write 操作。它主要用于内核 / 线程 / 组件之间发送 "事件信号",比 pipe 轻量得多。核心代码如下:
c
int nn_efd_init (struct nn_efd *self)
{
int rc;
int flags;
// EFD_CLOEXEC:进程 fork 后自动关闭,防止 fd 泄露
self->efd = eventfd (0, EFD_CLOEXEC);
// EMFILE 文件描述符数量超出进程限制,ENFILE文件描述符数量超出系统限制
if (self->efd == -1 && (errno == EMFILE || errno == ENFILE))
return -EMFILE;
errno_assert (self->efd != -1);
flags = fcntl (self->efd, F_GETFL, 0);
if (flags == -1)
flags = 0;
// 把 eventfd 设为非阻塞,避免 read/write 卡住。
rc = fcntl (self->efd, F_SETFL, flags | O_NONBLOCK);
errno_assert (rc != -1);
return 0;
}
// 这里是发送信号,一旦 write,epoll/poll 会立刻检测到该 fd 可读,阻塞立刻解除。
const uint64_t one = 1;
nbytes = write (fd, &one, sizeof (one));
// read 会读取当前计数器值,并把计数器清 0,让eventfd回到无符号的状态。
ssize_t sz = read (fd, &count, sizeof (count));
2.2、pipe
管道,linux会创建一对 fd:r 读端 + w 写端,往 w 写数据后r就会变成可读,数据存在内核缓冲区,可以被 epoll/poll/select 监听。其逻辑和eventfd相似,只不过有pipe2和pipe之分,pipe2可以直接设置执行关闭等属性。
c
int nn_efd_init (struct nn_efd *self)
{
int rc;
int flags;
int p [2];
#if defined NN_HAVE_PIPE2
// 高效版:一步创建 + 非阻塞 + 执行关闭
rc = pipe2 (p, O_NONBLOCK | O_CLOEXEC);
if ((rc == -1) && (errno == ENOTSUP)) {
rc = pipe (p); // 降级
}
#else
rc = pipe (p); // 传统 pipe
#endif
// 保存读端、写端,创建一个 非阻塞、自动关闭、安全的 pipe,r给epoll监听、w用来发信号
self->r = p [0];
self->w = p [1];
// 设置 CLOEXEC(fork 后自动关闭)
// 设置 NONBLOCK(非阻塞)
return 0;
}
// 发送信号(唤醒)
void nn_efd_signal (struct nn_efd *self)
{
char c = 101; // 随便发一个字节
write (self->w, &c, 1);
}
2.3、socketpair
socketpair(UNIX 域套接字对)模拟的事件通知器,功能和前两套(eventfd /pipe)完全一模一样。socketpair() 创建一对相互连接的 UNIX 域套接字(两个 fd),sp [0] 和 sp [1] 可以互相收发数据;像管道,但全双工、跨平台、更稳定;可以被poll/epoll/select/kqueue 监听;Windows /macOS/ Linux / BSD 全都支持。
c
int socketpair(int domain, int type, int protocol, int sv[2]);
// domain 必须为 AF_UNIX(或 AF_LOCAL)
// type 可以是 SOCK_STREAM(可靠双向流)或 SOCK_DGRAM(不可靠数据报)
// protocol 通常为 0
// sv[0] 和 sv[1] 是返回的两个文件描述符,向其中一个写入,可以从另一个读出,反之亦然。
3、nn_queue、nn_list、nn_hash
这里说一下,Nanomsg中的list、queue、hash都是侵入式数据结构,利用的还是nn_cont宏,设计的总体思路类似,都是有item、以及主结构体、有标记代表item不在主结构体中,在将item插入主结构体中时,都是需要先对item进行判断,只有它不在任何主结构体中时,才会将其插入主结构体。在删除item时,先判断它在主结构中,然后按照key去查询,找到之后,将其打上不在任何结构体的标记,并将结构体维护完善。和大多数哈希表一样,nn_hash同样具备扩充的功能,将槽位扩充为原来的两倍,并将原数据搬到新的哈希表中。
3.1、nn_queue
比较难理解的见注释
c
void nn_queue_push (struct nn_queue *self, struct nn_queue_item *item)
{
// 这里先对item的next进行判断,只是插入一个元素,next应该是初始化的状态
nn_assert (item->next == NN_QUEUE_NOTINQUEUE);
item->next = NULL;
if (!self->head)
self->head = item;
if (self->tail)
self->tail->next = item;
self->tail = item;
}
struct nn_queue_item *nn_queue_pop (struct nn_queue *self)
{
struct nn_queue_item *result;
if (!self->head)
return NULL;
result = self->head;
self->head = result->next;
if (!self->head)
self->tail = NULL;
// pop之后,应该把它赋值为初始化的状态
result->next = NN_QUEUE_NOTINQUEUE;
return result;
}
3.2、nn_list
c
// Nanomsg的结构体必须理解了这些宏的含义才能透彻的了解整个结构体的设计思路
/* Undefined value for initializing a list item which is not part of a list. */
#define NN_LIST_NOTINLIST ((struct nn_list_item*) -1)
/* Use for initializing a list item statically. */
#define NN_LIST_ITEM_INITIALIZER {NN_LIST_NOTINLIST, NN_LIST_NOTINLIST}
struct nn_list_item {
struct nn_list_item *next;
struct nn_list_item *prev;
};
struct nn_list {
struct nn_list_item *first;
struct nn_list_item *last;
};
struct nn_list_item *nn_list_erase (struct nn_list *self,
struct nn_list_item *item)
{
struct nn_list_item *next;
nn_assert (nn_list_item_isinlist (item));
if (item->prev)
item->prev->next = item->next;
else
self->first = item->next;
if (item->next)
item->next->prev = item->prev;
else
self->last = item->prev;
next = item->next;
// 和queue一样,擦除的时候,将其赋值为初始化的状态
item->prev = NN_LIST_NOTINLIST;
item->next = NN_LIST_NOTINLIST;
return next;
}
int nn_list_item_isinlist (struct nn_list_item *self)
{
return self->prev == NN_LIST_NOTINLIST ? 0 : 1;
}
3.3、nn_hash
需要注意的是,这里的nn_hash_item中key是有功能性的含义的,这个需要结合具体的用法,将这个key作为参数经过哈希函数会映射到不同的桶上,array是一个链表数组,这里就不对insert、erase函数进行记录了,只分析一个nn_hash_rehash函数。
c
#define NN_HASH_ITEM_INITIALIZER {0xffff, NN_LIST_ITEM_INITILIZER}
struct nn_hash_item {
uint32_t key;
struct nn_list_item list;
};
struct nn_hash {
uint32_t slots; // 桶的数量
uint32_t items; // 当前元素的个数
struct nn_list *array;
};
static void nn_hash_rehash (struct nn_hash *self)
{
uint32_t i;
uint32_t oldslots;
struct nn_list *oldarray;
struct nn_hash_item *hitm;
uint32_t newslot;
/* Allocate new double-sized array of slots. */
// 对老的哈希表进行备份
oldslots = self->slots;
oldarray = self->array;
// 对哈希表进行扩充
self->slots *= 2;
// 分配内存并初始化
self->array = nn_alloc (sizeof (struct nn_list) * self->slots, "hash map");
alloc_assert (self->array);
for (i = 0; i != self->slots; ++i)
nn_list_init (&self->array [i]);
/* Move the items from old slot array to new slot array. */
// 将老的哈希表中的数据搬移到新的哈希表
for (i = 0; i != oldslots; ++i) {
while (!nn_list_empty (&oldarray [i])) {
// 找元素
hitm = nn_cont (nn_list_begin (&oldarray [i]),
struct nn_hash_item, list);
// 把老的哈希表中的元素擦除
nn_list_erase (&oldarray [i], &hitm->list);
// 计算新的哈希值
newslot = nn_hash_key (hitm->key) % self->slots;
// 一般是插入在链表的尾部
nn_list_insert (&self->array [newslot], &hitm->list,
nn_list_end (&self->array [newslot]));
}
// 销毁老的链表
nn_list_term (&oldarray [i]);
}
/* Deallocate the old array of slots. */
// 销毁整个链表数组
nn_free (oldarray);
}