上一篇:DIY TCP/IP 网络设备模块4
5.4 网络设备模块的发送队列
网络设备层发送队列用于异步处理DIY TCP/IP上层模块发送的数据帧。为上层模块提供网络设备层的发送接口。上层模块的数据帧进入网络设备模块的发送队列后,唤醒发送线程,从发送队列中取出数据帧,通过PF_PACKET类型的socket,将数据帧逐个发送到Linux Kernel网络设备的驱动中。与网络设备模块接收队列的实现类似,首先修改网络设备结构体,添加发送队列相关的数据结构。
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;
/* tx */
int tx_sock;
pthread_t tx_thread;
pthread_cond_t txq_cond;
pthread_mutex_t txq_mutex;
queue_t txpkt_q;
/* dev info */
int ifindex;
} net_device_t;
Line 9: tx_sock文件描述符为PF_PACKET类型的socket。
Line 10-13: tx_thread是发送线程,txq_cond是发送线程的睡眠等待条件,txq_mutex是保护睡眠等待条件和发送队列的互斥量,txpkt_q是发送队列。
Line 15: ifindex, 网络接口的序号,指定发送数据帧时使用的linux kernel中的网络设备。
5.4.1 发送线程
dev_tx_init与dev_rx_init类似,用于初始化与发送线程相关的数据结构。在网络设备的初始化函数netdev_init中被调用。也有与销毁接收线程类似的dev_tx_deinit函数,在销毁网络设备时被调用。
static int dev_tx_init(net_device_t *ndev)
{
int ret = 0;
if (ndev == NULL)
return -1;
log_printf(DEBUG, "Network device TX init\n");
/* init tx queue wait condition */
if (pthread_cond_init(&ndev->rxq_cond, NULL)) {
log_printf(DEBUG, "Failed to init txq condition, %s (%d)\n",
strerror(errno), errno);
ret = -errno;
goto out;
}
/* init tx queue mutex */
if (pthread_mutex_init(&ndev->rxq_mutex, NULL)) {
log_printf(DEBUG, "Failed to init txq mutex, %s (%d)\n",
strerror(errno), errno);
ret = -errno;
goto out;
}
/* init tx thread */
terminate_loop = 0;
if(pthread_create(&ndev->tx_thread, NULL, dev_tx_routine, (void *)ndev)) {
log_printf(DEBUG, "Failed to create rx thread: %s (%d)\n",
strerror(errno), errno);
ret = -errno;
}
/* init tx packet queue */
queue_init(&ndev->txpkt_q);
/* create tx socket */
ndev->tx_sock = socket(AF_PACKET, SOCK_RAW, HSTON(ETH_P_ALL));
if (ndev->tx_sock < 0) {
log_printf(ERROR, "Failed to create tx socket\n");
ret = -1;
goto out;
}
out:
return ret;
}
Line 5-14: pthread_cond_init初始化发送线程的睡眠等待条件。
Line 15-21: pthread_mutex_init初始化保护睡眠等待条件和发送队列的互斥量互斥量。
Line 23-30: terminate_loop同样也是发送线程函数体dev_tx_routine的结束条件,初始值为0,该变量在netdev_stop_loop中被设置为1。pthread_create创建发送线程,指定发送线程函数体的入参为指向网络设备的指针netdev。dev_tx_routine从发送队列中取出上层模块发送的数据帧,通过PF_PACKET的socket将数据帧发送给Linux Kernel网络设备的驱动。queue_init初始化发送队列。
Line 31-39: 创建发送套接字tx_sock,域为AF_PACKET,类型是SOCK_RAW,协议为ETH_P_ALL。通过tx_socket发送的数据帧需包含802.3的头部,上层模块发送的数据帧,根据目标IP地址查找DIY TCP/IP ARP模块的地址映射表,为上层中添加802.3头部。Linux kernel的网络协议栈不对通过SOCK_RAW类型的socket发送的数据做任何改动,直接将数据发送到Linux kernel中与网络接口对应的设备的驱动。
创建和销毁函数成对出现是本人实现代码的习惯,目的是避免内存泄漏。再来介绍发送线程的销毁函数dev_tx_deinit。
static void dev_tx_deinit(net_device_t *ndev)
{
if (ndev == NULL)
return;
log_printf(DEBUG, "Network device TX deinit\n");
pthread_cond_signal(&ndev->txq_cond);
/* join tx thread */
if (pthread_join(ndev->tx_thread, NULL))
log_printf(DEBUG, "tx thread join failed, %s (%d)\n",
strerror(errno), errno);
/* flush dev tx queue */
dev_flush_txpktq(ndev);
/* destroy tx queue wait condition */
if (pthread_cond_destroy(&ndev->txq_cond))
log_printf(DEBUG, "txq condition destroy failed, %s (%d)\n",
strerror(errno), errno);
/* destroy tx queue mutext */
if (pthread_mutex_destroy(&ndev->txq_mutex))
log_printf(DEBUG, "txq mutex destroy failed, %s (%d)\n",
strerror(errno), errno);
/* close tx socket */
if (ndev->tx_sock)
close(ndev->tx_sock);
}
Line 1-12: pthread_cond_signal唤醒睡眠等待在txq_cond条件上的发送线程,pthread_join等待发送线程结束。dev_flush_txpktq清空发送队列,释放数据帧占用的内存。
Line 13-23: pthread_cond_destroy销毁睡眠等待条件txq_cond,pthread_mutex_destroy销毁互斥量txq_mutex。最后关闭发送套接字tx_sock。
再来修改网络设备初始化函数netdev_init和销毁函数netdev_deinit,分别添加对dev_tx_init和dev_tx_deinit的调用。
net_device_t * netdev_init(char *ifname)
{
…
/* rx init */
if (dev_rx_init(ndev))
goto out;
/* tx init */
if (dev_tx_init(ndev))
goto out;
return ndev;
out:
netdev_deinit(ndev);
return NULL;
}
Line 2-3: 省略的代码与5.1节netdev_init一致,dev_tx_init的调用放在接收逻辑的初始化函数dev_rx_init之后。
void netdev_deinit(net_device_t *ndev)
{
if (ndev == NULL)
return;
log_printf(DEBUG, "Network device deinit\n");
if (ndev->pcap_dev)
pcap_close(ndev->pcap_dev);
dev_rx_deinit(ndev);
dev_tx_deinit(ndev);
free(ndev);
gndev = NULL;
}
本节介绍与发送线程相关的初始化和销毁函数,其中涉及到清空发送队列的函数dev_flush_txpktq和发送线程的函数体dev_tx_routine。这两个函数的实现在下节引入。
5.4.2 发送队列
5.4.1节已经引入了发送线程的函数体dev_tx_routine,和发送队列的清空函数dev_flush_txpktq。本节除了介绍这两个函数的具体实现之外,还会引入新的函数netdev_tx_pkt,该函数是网络设备模块暴露给其他模块使用的接口,用于将上层模块的待发送数据帧加入网络设备模块的发送队列。
dev_tx_routine
static void *dev_tx_routine(void *args)
{
net_device_t *ndev = NULL;
queue_t *qnode = NULL;
dev_txpkt_t *txpkt = NULL;
if (args == NULL)
goto exit;
ndev = (net_device_t *)args;
while(!terminate_loop) {
pthread_mutex_lock(&ndev->txq_mutex);
pthread_cond_wait(&ndev->txq_cond, &ndev->txq_mutex);
pthread_mutex_unlock(&ndev->txq_mutex);
if (terminate_loop)
break;
again:
pthread_mutex_lock(&ndev->txq_mutex);
if (!queue_empty(&ndev->txpkt_q)) {
qnode = dequeue(&ndev->txpkt_q);
pthread_mutex_unlock(&ndev->txq_mutex);
txpkt = container_of(qnode, dev_txpkt_t, node);
dev_process_txpkt(txpkt);
goto again;
}
pthread_mutex_unlock(&ndev->txq_mutex);
}
exit:
log_printf(DEBUG, "Dev tx routine exited\n");
pthread_exit(0);
}
Line 9: 将入参args转换为网络设备结构体的指针。
Line 10-15: 发送线程的函数体是一个while循环,结束条件terminate_loop在创建发送线程时被初始化为0。只有在netdev_stop_loop被调用时,treminate_loop才会被置为1。while循环内先获得保护睡眠等待条件的互斥量,防止多线程并发访问。pthread_cond_wait检查睡眠等待条件,如果需要睡眠,pthread_cond_wait会释放txq_mutex互斥量,避免发送线程睡眠时仍然持有互斥量,然后让出CPU,使发送线程睡眠。如果发送线程被唤醒,pthread_cond_wait返回,pthred_cond_wait返回之前会再次获取互斥量txq_mutex,所以pthread_cond_wait返回后需要首先释放互斥量txq_mutex,然后判断退出条件terminate_loop是否为1,因为发送线程睡眠等待时terminate_loop有可能被置1。
Line 16-25: 发送线程循环体继续执行,获取保护发送队列的互斥量txq_mutex,判断发送队列不空时,取出队列中的节点,利用container_of宏获取封装queue_t的数据结构dev_txpkt_t的指针,交由dev_process_txpkt处理。跳转到again标号处继续处理发送队列中的数据帧,直到发送队列为空。
Line 26-30: 发送线程退出之前,pthread_exit设置返回值,pthread_join的调用者可以获取到该返回值。
dev_process_txpkt
static void dev_process_txpkt(dev_txpkt_t *txpkt)
{
if (txpkt == NULL)
return;
log_printf(VERBOSE, "process tx pkt\n");
if (txpkt->pdbuf)
free(txpkt->pdbuf);
free(txpkt);
}
DIY TCP/IP尚未实现任何上层模块,dev_process_txpkt仅仅是打印信息,表示发送线程被成功唤醒,并且取到了发送队列中的数据帧。不对发送数据做任何处理,先释放dev_txpkt_t中payload指向的内存,再释放dev_txpkt_t占用的内存。介绍DIY TCP/IP的ARP模块的实现时,再扩展dev_process_txpkt函数,通过PF_PACKET类型的socket发送数据帧。
dev_flush_txpktq
static void dev_flush_txpktq(net_device_t *ndev)
{
int flush_count = 0;
queue_t *qnode = NULL;
dev_txpkt_t *txpkt = NULL;
if (ndev == NULL)
return;
while(!queue_empty(&ndev->txpkt_q)) {
qnode = dequeue(&ndev->txpkt_q);
txpkt = container_of(qnode, dev_txpkt_t, node);
free(txpkt->pdbuf);
free(txpkt);
flush_count += 1;
}
log_printf(DEBUG, "Dev txq flushed %d packets\n", flush_count);
}
该函数在netdev_deinit中被调用,具体是在发送线程退出之后被调用。所以在清空发送队列时,不需要获取保护发送队列的互斥量。遍历发送队列中的节点,通过containter_of得到封装队列节点的dev_txpkt_t的指针。此时发送线程已经退出,直接释放payload指针指向的内存,和dev_txpkt_t数据结构占用的内存即可。
netdev_tx_pkt
void netdev_tx_pkt(void *pdbuf)
{
dev_txpkt_t *txpkt = NULL;
if (gndev == NULL || pdbuf == NULL) {
log_printf(ERROR, "Invalid net device tx parameter\n");
return;
}
txpkt = malloc(sizeof(dev_txpkt_t));
if (txpkt == NULL) {
log_printf(ERROR, "No memory for tx packet, %s (%d)\n",
strerror(errno), errno);
return;
}
txpkt->pdbuf = pdbuf;
/* enqueue tx pkt */
pthread_mutex_lock(&gndev->txq_mutex);
enqueue(&gndev->txpkt_q, &txpkt->node);
pthread_mutex_unlock(&gndev->txq_mutex);
/* wake up tx thread */
pthread_cond_signal(&gndev->txq_cond);
}
netdev_tx_pkt是网络设备模块暴露给DIY TCP/IP其他模块的接口,将数据帧加入网络设备模块的发送队列,唤醒发送线程异步处理待发送的数据帧。
Line 8-14: 为dev_txpkt_t数据结构分配内存,将上层模块的数据帧封装在dev_txpkt_t结构中,txpkt->pdbuf指向上层模块发送的数据帧。
Line 15-20: 获取txq_mutex互斥量,将dev_txpkt_t加入发送队列,释放互斥量txq_mutex,最后调用pthread_cond_signal唤醒发送线程。
DIY TCP/IP的上层模块尚未实现,本节发送队列的验证留在ARP模块的实现章节,当ARP模块收到ARP请求,并判断需要回复ARP Reply数据帧时,netdev_tx_pkt接口将被第一次调用,本节实现的代码也会得到验证。
下一篇:DIY TCP/IP 网络设备模块6