TCP/IP卷二 mbuf
概述
内核中的存储器管理调度直接关系到联网协议的性能
网络协议对内核的存储管理能力提出了很多要求。包括:
- 能方便地操作可变长缓存
- 能在缓存头部和尾部添加数据(如底层封装来自高层的数据)
- 能从缓存中移去数据(如当数据分二组向上经过新协议栈时要去掉首部)
- 尽量减少为这些操作所作的数据复制。
mbuf的主要用途是
- 保存在进程和网络接口间互相传递的用户数据
- 保存其他各种数据:源与目标地址、插口选项等等
四种类型的mbuf(依据成员m_flags填写的标志分类)
- m_flags=0,mbuf只含数据。mbuf中有108字节的数据空间(m_dat数组),m_data指向缓存的起始,但它能指向缓存中的任何位置。
- 第二类mbuf的m_flags值是M_PKTHDR,它指示这是一个分组首部,描述一个分组数据的第一个mbuf。数据仍然保存在这个mbuf中,但是由于分组首部占用了8字节,只有100字节的数据可存储在这个mbuf中。
- mbuf不包含分组首部(没有设置K_PKTHDR),但包含超过208字节的数据,用到簇的外部缓存(设置M_EXT)。在此mbuf中仍然为分组首部结构分配了空间,但没有用。
- mbuf包含一个分组首部,并包含超过208字节的数据(同时设置了M_PKTHDR和M_EXT)
应用mbuf的实例
mbuf结构
结构mbuf是用一个m_hdr结构跟着一个联合来定义的。
/* header at beginning of each mbuf: */
struct m_hdr {
struct mbuf *mh_next; /* next buffer in chain */
struct mbuf *mh_nextpkt; /* next chain in queue/record */
int mh_len; /* amount of data in this mbuf */
caddr_t mh_data; /* location of data */
short mh_type; /* type of data in this mbuf */
short mh_flags; /* flags; see below */
};
/* record/packet header in first mbuf of chain; valid if M_PKTHDR set */
struct pkthdr {
int len; /* total packet length */
struct ifnet *rcvif; /* rcv interface */
};
/* description of external storage mapped into mbuf, valid if M_EXT set */
struct m_ext {
caddr_t ext_buf; /* start of buffer */
void (*ext_free)(); /* free routine if not the usual */
u_int ext_size; /* size of buffer, for ext_free */
};
struct mbuf {
struct m_hdr m_hdr;
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]; /* !M_PKTHDR, !M_EXT */
} M_dat;
};
#define m_next m_hdr.mh_next
#define m_len m_hdr.mh_len
#define m_data m_hdr.mh_data
#define m_type m_hdr.mh_type
#define m_flags m_hdr.mh_flags
#define m_nextpkt m_hdr.mh_nextpkt
#define m_act m_nextpkt
#define m_pkthdr M_dat.MH.MH_pkthdr
#define m_ext M_dat.MH.MH_dat.MH_ext
#define m_pktdat M_dat.MH.MH_dat.MH_databuf
#define m_dat M_dat.M_databuf
指针m_next把mbuf链接成一个mbuf链表
指针m_nextpkt把mbuf链表连接成一个mbuf队列。
指针m_data指向相应缓存的开始(mbuf缓存本身或一个簇)。这个指针能指向相应缓存的任意位置,不一定是起始。
m_data指示存储在mbuf中的数据的类型。mbuf可以存储各种不同的数据结构。
m_flags值说明
m_len和分组首部中的成员m_pkthdr.len区别。m_pkthdr.len是链表中所有mbuf的成员m_len的总和。
mbuf宏和函数
宏——M开头的大写字母名称
函数——m_开始的小写字母名称。
宏在每个被用到的地方都被c预处理器展开(要求更多的代码空间),但是它在执行时更快。
函数在每个被调用的地方变成了一些指令,要求较少的代码空间,但会花费更多的执行时间。
m_get函数
这个函数仅仅是宏MGET的展开
/*
* Space allocation routines.
* These are also available as macros
* for critical paths.
*/
struct mbuf *
m_get(nowait, type)
int nowait, type;//nowait参数的值为M_WAIT或M_DONTWAIT,取决于在存储器不可用时是否要求等待
{
register struct mbuf *m;
MGET(m, nowait, type);
return (m);
}
MGET宏
调用MGET宏来分配存储
#define MGET(m, how, type) { \
MALLOC((m), struct mbuf *, MSIZE, mbtypes[type], (how)); \//内核宏MALLOC,通用内核存储器分配器进行的。mbtypes把mbuf的MT_XXX值转换为相应的M_XXX的值
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_retry函数
struct mbuf *
m_retry(i, t)
int i, t;
{
register struct mbuf *m;
m_reclaim();//每个协议都能定义一个"drain"函数,在系统缺乏可用存储器时能被m_reclaim调用
#define m_retry(i, t) (struct mbuf *)0 //m_retry被定义为一个空指针,这可以防止当存储器仍然不可用时的无休止的循环
MGET(m, i, t); //m_reclaim后可能有机会获得更多的存储器,因此再次调用MGET,试图获得mbuf
#undef m_retry
return (m);
}
mbuf锁
调用MBUFLOCK来保护函数和宏不被终端。由于mbuf在内核的所有层中被分配和释放,因此内核必须保护那些用于存储器分配的数据结构。
因为存储器分配与释放及簇分配与释放的宏被保护起来防止被终端,通常在MGET和m_get这样的函数和宏的前后不再调用spl函数
/*
* mbuf utility macros:
*
* MBUFLOCK(code)
* prevents a section of code from from being interrupted by network
* drivers.
*/
#define MBUFLOCK(code) \
{ int ms = splimp(); \
{ code } \
splx(ms); \
}
m_devget函数
m_devget,当接收2到一个以太网帧时,设备被驱动程序调用函数m_devget来创建一个mbuf链表,并把设备中的帧复制到这个链表中。根据所接受到的帧的长度(不包括以太网首部),可能导致4中不同的mbuf链表。
第一种的mbuf用于数据的长度在0-84字节之间的情况。在mbuf的开始保留了16字节。虽然14字节的以太网首部不存放在这里,但还是分配了一个14字节的用于输出的以太网首部。16字节而不是14字节的原因是为了在mbuf中庸长字对准方式存储IP首部
第二种的mbuf用于数据的长度在85-100字节之间。无16字节的保留空间。数据存储在数组m_pktdat的开始。
第三种mbuf用于数据的长度在101-207字节之间,要求有两个mbuf。前100个字节放在第一个mbuf中(有分组首部的mbuf),剩下的存放在第二个mbuf中。
第四种mbuf用于数据的长度超过或等于208字节,要用一个或多个簇的场景。
mtod和dtom宏
// * mtod(m,t) - convert mbuf pointer to data pointer of correct type
// * dtom(x) - convert data pointer within mbuf to mbuf pointer (XXX)
#define mtod(m,t) ((t)((m)->m_data))
#define dtom(x) ((struct mbuf *)((int)(x) & ~(MSIZE-1)))//MSIZE=128
mtod(“mbuf到数据”)返回一个只想mbuf数据的指针,并把指针声明为制定类型。
dtom(“数据到mbuf”)取得一个存放在一个mbuf中任意位置的数据的指针。并返回这个mbuf结构本身的一个指针。
MSIZE(128)是2的幂,内核存储器分配器总是为mbuf分配连续的MSIZE字节的存储快,dtom仅仅是清除参数中指针的低位来发现这个mbuf的起始位置。
dtom有一个问题,当它的参数指向一个簇,或者在一个簇内,它不能正确执行。因为那里没有指针从簇内指回mbuf结构。
m_pullup函数
m_pullup函数用来保证指定数目的字节(相应的协议首部的大小)在链表的第一个mbuf中紧挨着存放。即这些指定书目的字节被复制到一个新的mbuf并紧挨着存放。
m_pullup两个目的:1.当一个协议发现在第一个mbuf的数据量小于协议首部的最小长度时。调用m_pullup时基于假定协议首部的剩余部分存放在链表的下一个mbuf。m_pullup重新安排mbuf链表,使得前N字节的数据被连续地存放在链表的第一个mbuf中。
N是这个函数的一个参数必须<=100(MHLEN)
2.第二个用途涉及到IP和TCP的重组。
IP分片算法将各分片都存放在一个双向链表中,使用IP首部中的源与目标IP地址来存放向前与向后链表指针。如果IP首部在一个簇中,这些链表指针也会存放在这个簇中,导致以后遍历链表时,指向IP首部的指针不能被转换成指向mbuf的指针。
TCP报文段重组使用不同的技术而不是调用m_pullup。因为m_pullup开销较大:分配存储器并且数据从一个簇复制到一个mbuf中。TCP试图尽可能地避免数据的复制。除非TCP报文段被IP分片,否则不调用m_pullup。
TCP把mbuf指针存放在TCP首部中的一些未用的字段中,提供一个从簇指回mbuf的指针,来避免对每个失序的报文段调用m_pullup。
struct mbuf *
m_pullup(n, len)
register struct mbuf *n;
int len;
{
register struct mbuf *m;
register int count;
int space;
/*
* If first mbuf has no cluster, and has room for len bytes
* without shifting current data, pullup into it,
* otherwise allocate a new mbuf to prepend to the chain.
*/
if ((n->m_flags & M_EXT) == 0 &&
n->m_data + len < &n->m_dat[MLEN] && n->m_next) {
if (n->m_len >= len)
return (n);
m = n;
n = n->m_next;
len -= m->m_len;
} else {
if (len > MHLEN)
goto bad;
MGET(m, M_DONTWAIT, n->m_type);
if (m == 0)
goto bad;
m->m_len = 0;
if (n->m_flags & M_PKTHDR) {
M_COPY_PKTHDR(m, n);
n->m_flags &= ~M_PKTHDR;
}
}
space = &m->m_dat[MLEN] - (m->m_data + m->m_len);
do {
count = min(min(max(len, max_protohdr), space), n->m_len);
bcopy(mtod(n, caddr_t), mtod(m, caddr_t) + m->m_len,
(unsigned)count);
len -= count;
m->m_len += count;
n->m_len -= count;
space -= count;
if (n->m_len)
n->m_data += count;
else
n = m_free(n);
} while (len > 0 && n);
if (len > 0) {
(void) m_free(m);
goto bad;
}
m->m_next = n;
return (m);
bad:
m_freem(n);
MPFail++;
return (0);
}
m_copy和簇引用计数
簇的使用好处是多个mbuf间可以共享一个簇。由一个mbuf指向的簇(外部缓存)能通过m_copy被共享。例如,用于TCP输出,因为一个被传输的数据的副本要被发送端保存,直到数据被对方确认。
总结
mbuf的主要用途是在进程和网络接口之间传递用户数据时用来存放用户数据,但mbuf还用于保存其他各种数据:源地址和目的地址、插口选项等等。
mbuf几乎贯穿了协议栈的所有的函数,足见其重要性。
正确理解mbuf各种宏操作和函数调用极为关键,因为在后续的各种函数中,将会经常遇到