mbuf是memory buffer的缩写。它的主要用途是保存在进程和网络接口间互相传递的用户数据,同时也保存其它有用的信息,如源地址、插口选项等。
首先我们来看一下mbuf的数据结构,然后分析各成员变量的含义。
/* mbuf的头部数据结构,大小为20字节 */
struct m_hdr {
struct mbuf *mh_next;
/* 指向下一个mbuf这些buff存放一次应用的数据,send/recvr的一个分组数据可以很多*/
struct mbuf *mh_nextpkt;
/*指向下一个分组的第一个mbuf,可见这是所有分组的桥梁 */
caddr_t mh_data; /* 指向有效数据,可以是mbuf中的任意位置 */
int mh_len; /* 这个mbuf的有效数据长度*/
short mh_type; /* 数据类型? */
short mh_flags; // 见下图
/*mbuf标志位。如果为0,则mbuf只包含数据;
/如果为M_PKTHDR则说明这个mbuf 为一个分组的首部。*/
};
图1 首部的flags值
/*分组的首部,第一个mbuf; if M_PKTHDR */
struct pkthdr {
struct ifnet *rcvif;
/* 只有接收时对输入分组有用,否则对于输出分组置为0 */
int len; /* 分组的总长度*/
};
/* 如果使用外部存储 mbuf, valid if M_EXT set */
struct m_ext {
caddr_t ext_buf; /*指向外部存储数据位置*/
void (*ext_free)(); /* 不使用时释放外部存储 */
u_int ext_size; /* 外部存储的大小*/
};
struct mbuf {
struct m_hdr m_hdr;量//20字节
union {
struct {
struct pkthdr MH_pkthdr; /* M_PKTHDR set */
union {
struct m_ext MH_ext; /* M_EXT set */
char MH_databuf[MHLEN];
} MH_dat;
} MH;
char M_databuf[MLEN]; /* 只存放数据时最长为108字节*/
} M_dat;
};
由上面的数据结构,我们可以发现存在多种mbuf。
第一种就是m_flags等于0时,mbuf只包含数据,这个mbuf最大可容108个字节,内核定了mbuf最大为128字节,除去20字节的头部。
第二种m_flags等于M_PKTHDR,指示这是一个分组的首部,只首部增加一个m_pktdat结构,占去了8字节,如果不没有外部存储,则这个mbuf最大容纳100字节数据。
第三种mbuf包含外部存储,标志位M_EXT。
第四种mbuf既是首部同时还设置了M_EXT。
下图2展示了四种mbuf的结构,图3展示了一个队列的应用示意图。
图2 mbuf实例
图 3 一个队列示意图
struct mbuf * m_get(int nowait, int type;)
{
register struct mbuf *m;
MGET(m, nowait, type);
return (m);
}
#define MGET(m, how, type) { /
MALLOC((m), struct mbuf *, MSIZE, mbtypes[type], (how)); /
if (m) { /
(m)->m_type = (type); /
MBUFLOCK(mbstat.m_mtypes[type]++;) /
(m)->m_next = (struct mbuf *)NULL; /// 这个地方有点妙
(m)->m_nextpkt = (struct mbuf *)NULL; /
(m)->m_data = (m)->m_dat; /
(m)->m_flags = 0; /
} else /
(m) = m_retry((how), (type)); /
}
m_pullup函数用来保证制定数目的字节在链表的第一个m_buff中紧挨着存放,即这些指定数目的字节被复制到一个新的mbuff并紧挨着存放。什么时候会调用到m_pullup,我们要首先看一下其它几个函数
m_devget函数:当网络接收到一个网络帧时,设备驱动程序调用m_devget来创建一组mbuff链表,然后将这些数据复制到mbuff中。数据的多少导致可能创建不同的mbuff链表。在实际应用中,如果第一个mbuff中要16字节空出来,接收到的以太网首部其实不是存放在这里的,但还是要保留16个字节,也就是接收时16字节是浪费的。它的作用是在进行输出时将以太网地址存放在这里,这样就节省了建立输出数据报时的时间。使用16字节而不是以太网所需的14字节是为了用长字对准方式存储IP首部。
我们可以看到当数据少于84字节时,只要使用一个mbuff就可以了;对于85-100字节之间时仍用一个mbuff就可以了,只不过省略了16字节的头部了;大于100小于207字节的数据包,使用两个mbuff;大于207字节的数据包则需要使用外部存储了。
mtod和dtom宏:
#define mtod(m,t) ((t)((m)->m_data))
返回指向mbuf数据的指针,并把指针声名为指定的类型。
#define dtom(x) ((struct mbuf*)((int)(x)&~(MSIZE-1)))
取得一个存放在一个mbuf 中任意位置的数据指针并返回这个mbuf结构本身的一人指针。内核存储分配器总是分配连续的MSIZE(128)字节的块,dtom只是清除了参数中指针的低位来发现这个mbuf 的起始位置。这应该是基于mbuf起始地址总是128N,否则我觉得结论不一定正确。对于外部存储来说不能使用dtom,因为外部没有指回mbuf的结构,这里我们就可以使用m_pullup来解决问题。
m_pullup(),何时调用m_pullup()?当分组中第一个mbuf的数据量小于很小时,即小于标准的IP(20)+udp(8)/tcp(20)时调用。调用m_pullup是基于协议首部的剩余部分存放在链表的下一个mbuf中,m_pullup重新安排m_buf链表,使得协议首部的数据存放在第一个mbuf中,这样就可以使用mtod和dtom来操作。实际上上面这种情况很少使用也很少凑效。
第二个用途:应用在TCP/IP重组问题中。对于IP分组(10章以后需要回头来再看),因为IP的分组算法是基于IP包首部的源和目标地址而建立的双向链表。如果接收到的数据分组是存放在外部簇中,则它的IP首部将在外部簇中,由上面我们可以知道无法使用宏进行操作,这样在遍历链表时就不能转化为mbuf,所以不能将IP链表指针存储在簇中。解决方法:当接收到一个存放在簇中的分组,IP分片例程总是调用m_pullup强制性将20字节的IP首部放到它的mbuf中。新创建了一个 mbuf。这里也有缺陷就是m_pullup开销较大,不但要新分配一个mbuf还要将IP首部复制进来。TCP重组需要以后进一步学习。