上一篇:DIY TCP/IP 网络设备模块3
5.3.3 网络设备模块数据帧
本节结合5.3.2节链表和队列的实现,定义DIY TCP/IP网络设备模块数据帧的数据结构,以便用于队列操作。在pcap_callback返回接收到的数据帧后,将pcap_pkthdr封装成本节定义的数据帧,为实现网络设备模块的接收队列做准备。
DIY TCP/IP网络设备模块rx和tx数据帧的数据结构定义在device.h头文件中。
typedef struct _dev_rx_pkt {
queue_t node;
unsigned int seq;
unsigned int len;
unsigned char payload[0];
} __attribute__ ((packed)) dev_rxpkt_t;
typedef struct _dev_tx_pkt {
queue_t node;
unsigned int seq;
void *pdbuf;
} __attribute__ ((packed)) dev_txpkt_t;
struct _dev_rx_pkt封装了queue_t数据结构, seq和len是接收到的数据帧的序号和有效载荷的长度,payload指向链路层数据帧有效载荷的第一个字节。0长度数组是GNU C的用法,表示可变长数组,其本身只是一个占位符,sizeof计算dev_rxpkt_t数据结构的大小时,0长度数组占0个字节。
根据接收到的链路层数据帧的长度,为网络设备模块数据帧分配内存空间的大小为:sizeof(dev_rxpkt_t)加上有效载荷的长度。使用0长度数组,将有效载荷放在dev_rxpkt_t数据结构结束后的第一个字节,便于释放分配的内存空间。
struct _dev_tx_pkt同样封装了queue_t节点,seq为网络设备模块发送数据帧的序号,void *pdbuf,指向DIY TCP/IP上层模块发送的数据帧。上层模块发送的数据帧到达网络设备模块时已经具备了传输层TCP/UDP,网络层IP和以太网802.3的头部,这些头部在DIY TCP/IP的各模块实现时加入。网络设备模块只需根据802.3头部的目标地址将数据帧通过PF_PACKET类型的socket发送到linux kernel中对应的网络设备的驱动即可。
5.3.2节实现了队列的数据结构和操作函数,将queue_t数据结构封装在struct _dev_rx_pkt和struct _dev_tx_pkt中,利用node成员,基于队列的操作函数,即可实现网络设备模块的数据帧入队和出队操作。通过container_of又可以得到封装node成员的struct _dev_rx_pkt或struct _dev_tx_pkt的数据结构的指针。
5.3.4 接收队列
介绍了接收线程,链表,队列和网络设备模块数据帧的数据结构的实现之后,本节在这些基础上完成网络设备模块接收队列的实现。首先是修改网络设备结构体net_device_t数据结构,向其中加入接收队列相关的成员。
device.h
typedef struct _net_device {
pcap_t *pcap_dev;
/* rx */
pthread_t rx_thread;
pthread_cond_t rxq_cond;
pthread_mutex_t rxq_mutex;
queue_t rxpkt_q;
} net_device_t;
Line 7: 在net_device_t结构体中添加rxpkt_q成员,做为接收队列的队头节点。修改网络设备接收逻辑的初始化函数dev_rx_init,添加接收队列的初始化如下。
接收队列初始化
static int dev_rx_init(net_device_t *ndev)
{
...
/* init rx packet queue */
queue_init(&ndev->rxpkt_q);
out:
return ret;
}
Line 2-4略去的代码与5.3.1节一致。dev_rx_init函数中,在pthread_create创建接收线程之后,调用queue_init初始化网络设备的接收队列rxpkt_q。
修改pcap_callback函数,在收到链路层数据帧后,将其封装成dev_rxpkt_t数据类型,再放入网络设备的接收队列,最后唤醒接收线程处理接收队列中的数据帧。
static void pcap_callback(unsigned char *arg,
const struct pcap_pkthdr *pkthdr, const unsigned char *packet)
{
net_device_t *ndev = NULL;
dev_rxpkt_t *rxpkt = NULL;
unsigned int copy_len = 0;
if (packet == NULL || arg == NULL)
return;
ndev = (net_device_t *)arg;
log_printf(VERBOSE, "%ld.%06ld: capture length: %u, pkt length: %u\n",
pkthdr->ts.tv_sec, pkthdr->ts.tv_usec, pkthdr->caplen, pkthdr->len);
copy_len = sizeof(dev_rxpkt_t) + pkthdr->caplen;
rxpkt = (dev_rxpkt_t *)malloc(copy_len);
if (rxpkt == NULL) {
log_printf(DEBUG, "No memory for rx packet, %s (%d)\n",
strerror(errno), errno);
return;
}
memset(rxpkt, 0, copy_len);
rxpkt->len = pkthdr->caplen;
memcpy(rxpkt->payload, packet, pkthdr->caplen);
pthread_mutex_lock(&ndev->rxq_mutex);
enqueue(&ndev->rxpkt_q, &rxpkt->node);
pthread_mutex_unlock(&ndev->rxq_mutex);
pthread_cond_signal(&ndev->rxq_cond);
}
将解析链路层数据帧以太网头部的操作从pcap_callback函数中移除,它只需尽快将接收到的数据帧入队,即可返回。pcap_callback越快返回,pcap_loop库函数就可以越快的处理下一个链路层数据帧。入队后数据帧的解析和后续处理,在接收线程中并行完成。
Line 14-23: 根据链路层数据帧的长度,为dev_rxpkt_t数据结构申请内存。申请内存空间的大小为sizeof(dev_rxpkt_t)加上有效载荷的长度pkthdr->caplen。rxpkt->len保存有效载荷的长度,再将有效载荷packet复制到dev_rxpkt_t的payload指向的内存空间中。
Line 24-27: 获取保护接收队列的互斥量,将封装好的rxpkt加入网络设备的接收队列中,释放互斥量,最后调用pthread_cond_signal唤醒接收线程处理接收队列中的数据帧。
修改接收线程的主循环体dev_rx_routine
static void *dev_rx_routine(void *args)
{
net_device_t *ndev = NULL;
dev_rxpkt_t *rxpkt = NULL;
queue_t *qnode = NULL;
if (args == NULL)
goto exit;
ndev = (net_device_t *)args;
while(!terminate_loop) {
pthread_mutex_lock(&ndev->rxq_mutex);
pthread_cond_wait(&ndev->rxq_cond, &ndev->rxq_mutex);
pthread_mutex_unlock(&ndev->rxq_mutex);
if (terminate_loop)
break;
again:
pthread_mutex_lock(&ndev->rxq_mutex);
if (!queue_empty(&ndev->rxpkt_q)) {
qnode = dequeue(&ndev->rxpkt_q);
pthread_mutex_unlock(&ndev->rxq_mutex);
rxpkt = container_of(qnode, dev_rxpkt_t, node);
dev_process_rxpkt(ndev, rxpkt);
goto again;
}
pthread_mutex_unlock(&ndev->rxq_mutex);
}
exit:
printf("Dev rx routine exited\n");
pthread_exit(0);
}
Line 16-26: 接收线程阻塞在等待条件rxq_cond的睡眠队列上,被唤醒之后,先获取互斥量,判断接收队列不空时,从接收队列中取出队列节点。通过container_of获取封装队列节点的dev_rxpkt_t数据结构的指针,然后调用dev_process_rxpkt解析数据帧的payload。
static void dev_process_rxpkt(net_device_t *ndev, dev_rxpkt_t *rxpkt)
{
ethhdr_t *ethpkt = NULL;
if (ndev == NULL || rxpkt == NULL)
return;
ethpkt = (ethhdr_t *)rxpkt->payload;
printf("dev rx, ethernet type: %04x, "MACSTR " --> " MACSTR"\n",
NTOHS(ethpkt->type), MAC2STR(ethpkt->src), MAC2STR(ethpkt->dst));
free(rxpkt);
}
dev_process_rxpkt是device.c文件中新增的静态函数,解析dev_rxpkt_t的操作只是将rxpkt->payload强制转换为以太网头部数据类型,然后打印以太网头部的目的地址,源地址和头部类型。后续章节会不断的扩展dev_process_rxpkt函数,该函数根据以太网头部类型,将剥去以太网头部后的数据帧,交给DIY TCP/IP上层模块的接收函数处理。
DIY TCP/IP销毁时,需要清理接收队列,防止内存泄漏。添加网络设备模块接收队列的清理函数dev_flush_rxpktq。
static void dev_flush_rxpktq(net_device_t *ndev)
{
int flush_count = 0;
dev_rxpkt_t *rxpkt = NULL;
queue_t *qnode = NULL;
if (ndev == NULL)
return;
while(!queue_empty(&ndev->rxpkt_q)) {
qnode = dequeue(&ndev->rxpkt_q);
rxpkt = container_of(qnode, dev_rxpkt_t, node);
free(rxpkt);
flush_count += 1;
}
printf("dev flushed %d packets\n", flush_count);
}
该函数在销毁网络设备的接收逻辑的函数dev_rx_deinit中被调用,释放为dev_rxpkt_t数据帧申请的内存空间。接收线程退出后调用dev_flush_rxpktq,所以该函数操作网络设备的接收队列时不需要获取保护接收队列的互斥量。
while循环遍历网络设备的接收队列,通过containter_of得到dev_rxpkt_t的指针。此时接收线程已经退出,没有必要再处理接收队列中的数据帧,直接释放分配的内存空间即可。
修改dev_rx_deinit函数
static void dev_rx_deinit(net_device_t *ndev)
{
if (ndev == NULL)
return;
printf("Network device RX deinit\n");
pthread_cond_signal(&ndev->rxq_cond);
/* join rx thread */
if (pthread_join(ndev->rx_thread, NULL))
printf("rx thread join failed, %s (%d)\n",
strerror(errno), errno);
/* flush dev rx queue */
dev_flush_rxpktq(ndev);
/* destroy rx queue wait condition */
if (pthread_cond_destroy(&ndev->rxq_cond))
printf("rxq condition destroy failed, %s (%d)\n",
strerror(errno), errno);
/* destroy rx queue mutext */
if (pthread_mutex_destroy(&ndev->rxq_mutex))
printf("rxq mutex destroy failed, %s (%d)\n",
strerror(errno), errno);
}
Line 11-12: pthread_join返回后,表明接收线程已经退出。调用dev_flush_rxpktq清空接收队列。至此网络设备模块的接收队列已经实现完成,编译运行结果如下:
gannicus@ubuntu:~/guojia/tasks/DIY_USER_SPACE_TCPIP/ch2/2$ sudo ./tcp_ip_stack
[sudo] password for gannicus:
Network device init
filter: ether proto 0x0800 or ether proto 0x0806
Network device RX init
1533030301.608931: capture length: 78, pkt length: 78
1533030301.858040: capture length: 143, pkt length: 143
1533030301.858057: capture length: 171, pkt length: 171
dev rx, ethernet type: 0800, 60:eb:69:c3:51:70 --> 00:19:2f:91:cd:ff
dev rx, ethernet type: 0800, 00:19:2f:91:cd:ff --> 60:eb:69:c3:51:70
dev rx, ethernet type: 0800, 60:eb:69:c3:51:70 --> 00:19:2f:91:cd:ff
1533030302.070609: capture length: 139, pkt length: 139
1533030302.088789: capture length: 60, pkt length: 60
1533030302.398916: capture length: 215, pkt length: 215
…
^Cpcap_loop ended
Network device deinit
Network device RX deinit
Dev rx routine exited
dev flushed 0 packets
从运行结果可以看出,tcp_ip_stack的主线程中调用pcap_callback打印从链路层接收到的数据帧的长度和时间戳。与主线程并行执行的网络设备模块的接收线程从接收队列中取出数据帧,解析以太网头部,打印以太网头部信息。终端键入ctrl+c后,销毁网络设时,清空接收队列,接收队列被清空时没有尚未处理的数据帧。
下一篇:DIY TCP/IP 网络设备模块5