SO_TIMESTAMP 数据链路层的接收时间戳

本文解析了Linux网络栈中数据报的接收流程,包括如何为数据报添加时间戳、如何利用softnet_data结构体进行数据接收队列管理、如何通过netif_rx_schedule触发软中断处理接收队列等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    网络设备在接收到来自网络中其它主机的数据报,或本地环回接口的数据报之后,交给协议栈的netif_rx函数,该函数首先要为收到的这个skb打上当前的时间戳(skb->tstamp成员),这个时间戳表示该数据到达的时间,它不是必选的,可以通过套接字选项SO_TIMESTAMP将其打开,该选项打开时间戳时,会将链路层的全局变量netstamp_needed加1,netif_rx在检查到这个变量不为零时,为skb打上时间戳。


    softnet_data是类型为struct softnet_data结构体的全局变量,每个CPU定义一个,它是链路层的数据接收队列,该结构体的定义如下:
    struct softnet_data
    {
        struct net_device   *output_queue;
        struct sk_buff_head input_pkt_queue;
        struct list_head    poll_list;
        struct sk_buff      *completion_queue;
        struct net_device   backlog_dev;
    };

    input_pkt_queue是skb的队列,接收到的skb全部进入该队列等待后续处理,netif_rx首先检查该队列当前的长度input_pkt_queue.qlen,即当前排在队列中的skb的数量,当数量超过netdev_max_backlog的值时,直接丢弃新收到的包,netdev_max_backlog在协议栈中定义的缺省值为1000,可以通过文件/proc/sys/net/core/netdev_max_backlog进行修改。如果当前队列长度未达到上限,把新收到的skb加到这个队列中,在加到队列之前,要确保对这个队列的接收处理已启动,如果当前队列为空,则要先调用netif_rx_schedule启动队列的处理,再把skb加到队列中。需要注意的是softnet_data是CPU绑定的,但不是网络设备绑定的,多个网络设备收到的数据报可能存放在同一个队列中待处理。


    netif_rx_schedule函数的主要作用是触发一个软中断NET_RX_SOFTIRQ,使中断处理函数net_rx_action处理接收队列中的数据报。net_rx_action开始时会记录下系统的当前时间,然后进行处理,当处理时间持续超过1个时钟嘀嗒时,它会再触发一个中断NET_RX_SOFTIRQ,并退出,在下一个中断中继续处理。一次中断处理除了时间上有限制,处理的数据报的数量上也有限制。


    softnet_data的成员poll_list中存放的是成员backlog_dev的地址,由netif_rx_schedule存入,backlog_dev的成员poll在系统初始化时被指向函数process_backlog,net_rx_action调用该函数进行实际的数据报处理,process_backlog把数据报从input_pkt_queue队列中取出,传给netif_receive_skb,由netif_receive_skb传给相应的网络层接收函数。process_backlog的处理时间也有1个时钟嘀嗒的限制,同时一次处理的数据报的数量不得超过backlog_dev->quota和netdev_budget两个值中较小的那个值,backlog_dev->quota由netif_rx_schedule初始化为全局变量weight_p的值,缺省为64,netdev_budget缺省为300。从代码可以看出,process_backlog一次处理最大数据报数量为64,而net_rx_action为300。weight_p和netdev_budget这两个值分别可以在文件/proc/sys/net/core/dev_weight和/proc/sys/net/core/netdev_budget中查看和修改。


    netif_receive_skb是链路层接收数据报的最后一站。它根据注册在全局数组ptype_all和ptype_base里的网络层数据报类型,把数据报递交给不同的网络层协议的接收函数(INET域中主要是ip_rcv和arp_rcv)。


ModbusMasterStatus modbus_write_single_register( ModbusMasterContext* ctx, uint8_t slave_addr, uint16_t reg_addr, uint16_t value) { if(!ctx) return MB_MASTER_INVALID_RESPONSE; ModbusMasterStatus final_status = MB_MASTER_TIMEOUT; uint8_t retries = 0; // 构建请求帧 ctx->tx_buffer[0] = slave_addr; ctx->tx_buffer[1] = 0x06; // 功能码 *(uint16_t*)(ctx->tx_buffer + 2) = BSWAP16(reg_addr); *(uint16_t*)(ctx->tx_buffer + 4) = BSWAP16(value); uint16_t crc = modbus_crc16(ctx->tx_buffer, 6); *(uint16_t*)(ctx->tx_buffer + 6) = crc; while(retries <= ctx->config.retry_count) { // 切换发送模式 ctx->hal.rs485_dir_ctrl(true); if(!ctx->hal.uart_transmit(ctx->tx_buffer, 8)) { final_status = MB_MASTER_NETWORK_ERROR; break; } // 切换接收模式 ctx->hal.rs485_dir_ctrl(false); ctx->last_op_time = ctx->hal.get_timestamp(); ctx->state = MB_MASTER_BUSY; // 等待响应 uint32_t timeout = ctx->config.response_timeout; while((ctx->hal.get_timestamp() - ctx->last_op_time) < timeout) { int rx_len = ctx->hal.uart_receive(ctx->rx_buffer, sizeof(ctx->rx_buffer)); if(rx_len == 8) { // 正确响应长度应为8字节 if(validate_response(ctx, 0x06, slave_addr, rx_len)) { // 解析响应数据 uint16_t resp_addr = BSWAP16(*(uint16_t*)(ctx->rx_buffer + 2)); uint16_t resp_value = BSWAP16(*(uint16_t*)(ctx->rx_buffer + 4)); if(resp_addr == reg_addr && resp_value == value) { final_status = MB_MASTER_READY; } else { final_status = MB_MASTER_INVALID_RESPONSE; } break; } } } if(final_status == MB_MASTER_READY) break; // 等待帧间隔 uint32_t frame_delay = calc_interframe_delay(ctx->config.baudrate); while((ctx->hal.get_timestamp() - ctx->last_op_time) < frame_delay); retries++; } ctx->state = MB_MASTER_READY; return final_status; } 以上函数是modbusRTU协议的功能实现代码 // 初始化Modbus上下文 ModbusMasterContext ctx; const ModbusMasterHAL hal = { .rs485_dir_ctrl = RS485_DIR_CTRL, .uart_transmit = UART_Transmit, .uart_receive = UART_Receive, .get_timestamp = Get_Timestamp }; modbus_master_init(&ctx); // 设置通信参数 ctx.config.baudrate = 9600; // 波特率9600 ctx.config.retry_count = 3; // 执行写寄存器操作 ModbusMasterStatus status = modbus_write_single_register( &ctx, 0x01, // 从机地址 0x2000, // 寄存器地址(2000H) 0x0001 // 写入值(0001H) ); // 处理操作结果 if(status == MB_MASTER_READY) { // 写入成功处理 } else { // 错误处理 ModbusMasterStatus status = modbus_write_single_register( &ctx, 0x01, // 从机地址 0x2000, // 寄存器地址(2000H) 0x0005 // 写入值(0001H) ); } 以上函数是主函数中控制端口发送信息的代码,若我想要在keil中看到传输信息的具体数据,应该将什么放进watch部分
03-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值