Linux内核packet_rcv代码分析

本文详细解析了Linux内核中packet_rcv函数的工作流程。该函数用于处理网络设备接收到的数据包,涉及 skb 结构的调整、数据包过滤、内存管理以及将数据包放入接收队列等关键步骤。

上码分析:

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,

     struct packet_type *pt, struct net_device *orig_dev)
{
struct sock *sk;
struct sockaddr_ll *sll;
struct packet_sock *po;
u8 *skb_head = skb->data;
int skb_len = skb->len;
unsigned int snaplen, res;

/*

skb->pkt_type表示帧的类型,分类是由L2的目的地址来决定的,可能的取值都在if_packet.h中定义,对于以太网设备来说,这个变量由eth_type_trans函数初始化。

PACKET_HOST 包的目的地址与收到它的网络设备的L2地址相等,即这个包是发给本机的

PACKET_MULTICAST 包的目的地址是一个多播地址,而这个多播地址时收到这个包的网络设备所注册的多播地址

PACKET_BROADCAST 包的目的地址是一个广播地址,而这个广播地址也是收到这个包的网络设备的广播地址

PACKET_OTHERHOST 包的目的地址与收到它的网络设备地址完全不同,因此,如果本机的转发功能没有启动,这个包会被丢弃。

PACKET_OUTGOING 这个包将被发出。用稿这个标记的功能包括Decnet协议,或者是为每个网络tap都复制一份发出包的函数。

PACKET_LOOPBACK 这个包发向loopback设备。由于有这个标记,在处理loopback设备室,内核可以跳过一些真实设备才需要的操作。

PACKET_FASTROUTE 这个包有快速路由代码查找路由。快速路由功能在2.6内核中已经去掉了。

*/


if (skb->pkt_type == PACKET_LOOPBACK)
goto drop;


sk = pt->af_packet_priv;
po = pkt_sk(sk);


if (!net_eq(dev_net(dev), sock_net(sk)))
goto drop;


skb->dev = dev;


if (dev->header_ops) {
/* The device has an explicit notion of ll header,
* exported to higher levels.
*
* Otherwise, the device hides details of its frame
* structure, so that corresponding packet head is
* never delivered to user.

*/

/*

if (sk->sk_type != SOCK_DGRAM) 这个负责区分应用层SOCK_RAW和SOCK_DGRAM,SOCK_RAW包含L2的数据头,运行此packet_rcv函数时,skb->data指向L3层头,所以如果是SOCK_RAW需要重设skb->data

*/

if (sk->sk_type != SOCK_DGRAM)
skb_push(skb, skb->data - skb_mac_header(skb));
else if (skb->pkt_type == PACKET_OUTGOING) {
/* Special case: outgoing packets have ll header at head */
skb_pull(skb, skb_network_offset(skb));
}
}


snaplen = skb->len;

/*

过滤规则,BPF数据包过滤(BSD PACKET Filter)

*/

res = run_filter(skb, sk, snaplen);
if (!res)
goto drop_n_restore;
if (snaplen > res)
snaplen = res;


if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=
   (unsigned)sk->sk_rcvbuf)
goto drop_n_acct;


if (skb_shared(skb)) {
struct sk_buff *nskb = skb_clone(skb, GFP_ATOMIC);
if (nskb == NULL)
goto drop_n_acct;


if (skb_head != skb->data) {
skb->data = skb_head;
skb->len = skb_len;
}
kfree_skb(skb);
skb = nskb;
}


BUILD_BUG_ON(sizeof(*PACKET_SKB_CB(skb)) + MAX_ADDR_LEN - 8 >
    sizeof(skb->cb));


sll = &PACKET_SKB_CB(skb)->sa.ll;
sll->sll_family = AF_PACKET;
sll->sll_hatype = dev->type;
sll->sll_protocol = skb->protocol;
sll->sll_pkttype = skb->pkt_type;
if (unlikely(po->origdev))
sll->sll_ifindex = orig_dev->ifindex;
else
sll->sll_ifindex = dev->ifindex;


sll->sll_halen = dev_parse_header(skb, sll->sll_addr);


PACKET_SKB_CB(skb)->origlen = skb->len;


if (pskb_trim(skb, snaplen))
goto drop_n_acct;


skb_set_owner_r(skb, sk);
skb->dev = NULL;
skb_dst_drop(skb);


/* drop conntrack reference */
nf_reset(skb);


spin_lock(&sk->sk_receive_queue.lock);
po->stats.tp_packets++;
skb->dropcount = atomic_read(&sk->sk_drops);
__skb_queue_tail(&sk->sk_receive_queue, skb);//把数据包放到接收队列中,等待应用层recv操作。
spin_unlock(&sk->sk_receive_queue.lock);
sk->sk_data_ready(sk, skb->len);
return 0;


drop_n_acct:
spin_lock(&sk->sk_receive_queue.lock);
po->stats.tp_drops++;
atomic_inc(&sk->sk_drops);
spin_unlock(&sk->sk_receive_queue.lock);


drop_n_restore:
if (skb_head != skb->data && skb_shared(skb)) {
skb->data = skb_head;
skb->len = skb_len;
}
drop:
consume_skb(skb);
return 0;

}


分析暂时到此,大概了解一下,以后有新的发现再更新。

<think> 我们正在讨论的是调制解调器(Modem)如何在内核中注册`packet_rcv`函数作为数据包接收处理函数。在Linux内核中,网络设备驱动通过注册一个`packet_type`结构来指定特定协议的数据包应该由哪个函数处理。调制解调器设备(通常是PPP或蜂窝网络设备)同样需要将自己的数据包处理函数注册到网络子系统中。 根据引用[1]和[2]提供的信息,我们知道: - 引用[1]提到,UDP接收函数`udp_rcv`通过查找内核中的sock结构来处理数据包。 - 引用[2]则提到,PPP驱动通过`ioctl`系统调用将通道绑定到PPP驱动,并最终调用到内核的`ppp_ioctl`函数。 对于调制解调器(通常作为PPP设备或虚拟网络设备)注册`packet_rcv`函数,实际上是指调制解调器驱动如何设置其数据包接收处理函数。调制解调器设备通常不是以太网设备,因此它不会直接使用`af_packet.c`中的`packet_rcv`函数。但是,如果我们要让调制解调器的数据包也能被AF_PACKET套接字捕获,那么我们需要确保调制解调器设备的数据包能够被传递到AF_PACKET的协议处理函数。 下面详细讲解调制解调器设备注册数据包接收函数的方法,特别是如何与`af_packet.c`中的`packet_rcv`关联: ### 调制解调器注册接收函数的方法 1. **调制解调器设备驱动注册接收函数** 调制解调器设备驱动(比如PPP驱动、USB蜂窝调制解调器驱动等)在初始化时,会创建一个网络设备(`struct net_device`)。当这个网络设备被打开(`ndo_open`)时,驱动会设置数据包接收处理函数。对于PPP设备,通常使用`ppp_input`函数。对于其他类型的调制解调器,可能有自己的处理函数。 但是,AF_PACKET套接字(SOCK_RAW或SOCK_DGRAM)需要捕获所有网络设备上的数据包。为了实现这一点,内核中有一个全局的`ptype_all`链表,它用于存放所有希望接收所有类型数据包的处理函数(包括`packet_rcv`)。此外,还有根据协议类型组织的`ptype_base`哈希表。 2. **AF_PACKET的注册** 在`af_packet.c`中,当创建一个AF_PACKET套接字时(比如通过`socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))`),内核会调用`packet_create`函数。在这个函数中,会初始化一个`packet_sock`结构,并将该套接字注册到全局的协议处理链表中。具体来说,它通过`dev_add_pack`函数将一个`packet_type`结构添加到`ptype_all`链表(如果协议是`ETH_P_ALL`)或者`ptype_base`哈希表(指定协议)中。 这个`packet_type`结构中的`.func`字段就指向`packet_rcv`函数。 3. **调制解调器数据包如何到达`packet_rcv`** 当调制解调器设备收到一个数据包时,它会通过`netif_rx`或`netif_receive_skb`函数将数据包送入协议栈。在这个流程中,内核会遍历`ptype_all`链表中的所有处理函数,然后再根据协议类型从`ptype_base`中找到对应的处理函数,逐个调用。因此,如果AF_PACKET套接字注册了`ETH_P_ALL`,那么它的`packet_rcv`函数就会被调用。 4. **调制解调器驱动需要做什么?** 调制解调器驱动本身不需要直接调用`packet_rcv`函数,它只需要按照标准网络设备驱动的方式,在接收到数据包时,将数据包转换成`sk_buff`结构,然后调用`netif_rx`或`netif_receive_skb`将数据包发送到网络协议栈。协议栈会负责将数据包分发给所有注册的协议处理函数,包括AF_PACKET套接字。 例如,在PPP驱动中,当收到一个数据包时,会调用`ppp_input`函数,而在`ppp_input`函数内部,最终会调用`__netif_rx`或`netif_rx_ni`将数据包送入协议栈。 5. **特殊处理:非以太网设备** 调制解调器设备通常不是以太网设备,因此它们的数据包可能没有标准的以太网头部。AF_PACKET套接字在接收非以太网设备的数据包时,需要特殊处理。在`packet_rcv`函数中,会检查设备类型(`dev->type`)。如果是非以太网设备(如`ARPHRD_PPP`),则会在数据包前面添加一个伪以太网头部(通过`sll`结构),或者根据套接字选项决定是否保留原始链路层头部。 在高通平台(如引用[4]提到的上行信道通讯协议),调制解调器设备可能是`ARPHRD_RAWIP`类型。在`packet_rcv`函数中,对于这种类型的设备,会跳过虚拟的以太网头部(如果有的话),以便正确处理原始IP数据包。 ### 注册流程总结 1. **调制解调器驱动初始化**: - 创建网络设备(`struct net_device`),设置`net_device_ops`(包括打开、关闭、发送等操作)。 - 在打开设备时,启动调制解调器硬件,准备接收数据包。 2. **接收数据包**: - 当调制解调器收到数据包后,驱动分配一个`sk_buff`,将数据放入其中。 - 设置`skb->dev`为对应的网络设备。 - 调用`netif_rx(skb)`或`netif_receive_skb(skb)`将数据包交给协议栈。 3. **协议栈处理**: - 协议栈调用所有注册在`ptype_all`链表上的处理函数(包括AF_PACKET套接字注册的`packet_rcv`)。 - 根据数据包的协议类型,调用注册在`ptype_base`中的处理函数(例如IP协议处理函数)。 4. **AF_PACKET套接字接收**: - `packet_rcv`函数被调用,它根据套接字设置(如过滤规则、是否启用零拷贝等)将数据包放入套接字的接收队列或环形缓冲区。 - 唤醒等待该套接字的用户空间进程。 ### 调制解调器驱动示例代码片段 以下是一个简化的调制解调器驱动接收数据包的示例: ```c static void modem_rx_packet(struct modem_device *modem, void *data, int len) { struct sk_buff *skb; struct net_device *dev = modem->netdev; /* 分配sk_buff,注意这里可能需要根据设备类型设置协议头 */ skb = netdev_alloc_skb(dev, len); if (!skb) { /* 处理错误 */ return; } /* 复制数据到sk_buff */ skb_put(skb, len); memcpy(skb->data, data, len); /* 设置设备 */ skb->dev = dev; skb->protocol = htons(ETH_P_IP); // 假设是IP数据包 /* 提交到协议栈 */ if (netif_rx(skb) == NET_RX_SUCCESS) { /* 接收成功 */ } else { /* 接收失败 */ } } ``` ### 相关问题 1. 调制解调器设备驱动和普通以太网设备驱动在数据包接收处理上有何不同? 2. 如何为AF_PACKET套接字设置过滤器以捕获特定的调制解调器数据包? 3. 在非以太网设备上使用AF_PACKET套接字时,链路层头部的处理方式有什么不同? 4. 如何配置AF_PACKET套接字以接收调制解调器设备的原始数据包? </think> ### Modem 注册 `packet_rcv` 函数的方法详解 在 Linux 内核中,调制解调器(Modem)注册 `af_packet.c` 文件中的 `packet_rcv` 函数作为数据包接收处理函数,主要通过以下核心机制实现: #### 1. **驱动层注册网络设备** 调制解调器驱动通过 `net_device_ops` 结构向内核注册网络接口,关键操作包括: ```c static const struct net_device_ops modem_netdev_ops = { .ndo_open = modem_open, .ndo_stop = modem_close, .ndo_start_xmit = modem_xmit, // 发送函数 .ndo_rx = modem_rx_handler, // 接收入口 }; ``` 在初始化时调用 `register_netdev()` 完成注册。当 Modem 收到数据包时,内核会调用 `modem_rx_handler` 作为接收入口点[^1]。 #### 2. **对接协议栈处理层** 在接收处理函数中,驱动通过 `netif_rx()` 或 `netif_receive_skb()` 将原始数据包提交给网络协议栈: ```c static void modem_rx_handler(struct sk_buff *skb) { skb->dev = modem_dev; // 关联Modem设备 skb->protocol = eth_type_trans(skb, modem_dev); netif_rx(skb); // 提交到协议栈 } ``` 此时数据包进入内核协议栈分发流程[^1]。 #### 3. **关联 AF_PACKET 协议处理器** 在 `af_packet.c` 中,通过 `dev_add_pack()` 注册全局包处理函数: ```c static struct packet_type modem_packet_type = { .type = cpu_to_be16(ETH_P_ALL), // 捕获所有协议 .func = packet_rcv, // 指向目标处理函数 }; void modem_init(void) { dev_add_pack(&modem_packet_type); // 注册到内核 } ``` 关键参数说明: - `.type = ETH_P_ALL`:捕获所有类型数据包 - `.func = packet_rcv`:指定 `af_packet.c` 中的处理函数 - `dev_add_pack()` 将此处理器加入全局链表 `ptype_all`[^1] #### 4. **数据包分发流程** 当 Modem 数据包到达协议栈时,内核通过以下路径分发: $$ \begin{align*} \text{Modem硬件} & \xrightarrow{\text{中断}} \text{modem\_rx\_handler} \\ & \xrightarrow{\text{构建sk\_buff}} \text{netif\_receive\_skb()} \\ & \xrightarrow{\text{协议分发}} \text{packet\_rcv()} \end{align*} $$ 在 `packet_rcv()` 中完成: 1. BPF 过滤检查 2. 时间戳记录 3. 数据包入队(环形缓冲区或接收队列) 4. 唤醒用户进程[^1] #### 5. **Modem 特殊处理** 针对蜂窝网络特性需额外处理: 1. **链路层适配** 移除调制解调器特有的伪头部: ```c if (skb->dev->type == ARPHRD_RAWIP) { skb_pull(skb, 14); // 移除虚拟以太网头 } ``` 2. **协议标识** 设置蜂窝网络特有协议类型: ```c skb->protocol = htons(ETH_P_MAP); // 3GPP TS 29.060 ``` 3. **零拷贝优化** 在 DMA 映射中使用 Modem 专用 API: ```c dma_map_single(modem_dma_dev, skb->data, len, DMA_FROM_DEVICE); ``` #### 6. **注册验证方法** 1. 查看内核日志确认注册: ```bash dmesg | grep "register_netdev" # 输出示例:modem0 registered ``` 2. 检查协议处理器链表: ```c // 在代码中添加调试语句 printk("Modem packet_type registered at %px\n", &modem_packet_type); ``` ### 典型数据流 ```mermaid sequenceDiagram Modem硬件->>+驱动层: 硬件中断(RX) 驱动层->>+协议栈: netif_receive_skb(skb) 协议栈->>+packet_rcv: 通过ptype_all链表分发 packet_rcv->>+用户空间: skb入队环形缓冲区 用户空间-->>-内核: read()/mmap访问 ``` ### 相关问题 1. 如何在 Modem 驱动中实现零拷贝数据传递? 2. 蜂窝网络数据包需要哪些特殊处理? 3. 如何验证 `packet_rcv` 是否成功处理 Modem 数据包? 4. `dev_add_pack()` 和 `netif_rx()` 的协作机制是什么? [^1]: 网络设备驱动与协议栈交互机制 [^2]: 蜂窝网络协议的特殊处理要求
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值