UDP底层与守护进程化-报头结构体skbuff指针移动与自成会话
UDP 底层与守护进程化:报头结构体、sk_buff 指针移动与自成会话
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
源笔记:udp底层1(26-9-19)、udp底层2(26-9-20)、将网络服务守护进程化(26-9-20)、操作系统本身是由大量中断构成的(26-9-21)、udp最核心/守护进程2(26-9-22)
HTTP 章收官时留过一句话:“想要了解 xshell 的工作原理,还差守护进程”。现在补上这块拼图,同时往下钻一层:UDP 报文在内核里怎么流转、read 一个网络文件时数据怎么到你手上的。这一篇把 UDP 底层、硬软中断、守护进程化串成一条线——内核里的数据通路和用户态服务的生存之道。
一、先看报头:UDP 的 8 字节
回到当初写代码时的问题:为什么端口号是 16 位的?IP 是 32 位点分 4 段?
答案:这是内核 UDP 协议(报头格式)决定的——报头里源端口、目的端口各占 16 位,IP 地址 32 位。业界常用协议的端口号都是固定的(HTTP 80、SSH 22),这样才能"按号入座"。
源 IP + 目的 IP + 源端口 + 目的端口,可以表示互联网上唯一的一个进程!
再看有效载荷怎么分离?和手搓协议找 \r\n、HTTP 找空行一个道理,只是 UDP 更极端:UDP 的报头定长 8 字节(源端口 2 + 目的端口 2 + 16 位 UDP 长度 + 16 位校验和)——其中16 位 UDP 长度描述整个 UDP 报文的长度。长度 2 字节,决定了 UDP 单个报文最多 64K 数据(还要包括报头!)——这也是 UDP 不适合传大文件的根本原因。
HTTP 的报头动辄几十字节(方法、URL、版本一长串),UDP 8 字节,这就是"轻"的来源。
二、报头的本质:内核眼里的结构体
UDP 报头,本质是一个结构体!
- 应用层看到的永远是字符串:read/recv/send/sendto 收发的都是字节流;
- 内核看到的永远是结构体:传输层拿到数据,直接把指针往上一压,按结构体的度量衡识别——字段位置、长度全部天然对齐,不需要繁杂的序列化和反序列化!
所以同一个报文有两个身份:应用层视角是字符串(所以要自己序列化),内核视角是结构体(所以天然高效)。
封装的本质:结构体变量的拷贝
发送"你好"两个字时内核干了什么?
得到"你好",内核指针 p 指向"你"的位置,
p - sizeof(要封装的报头类型),腾出报头空间,把报头字段字节级别拷贝进去——封装完成。
封装的本质,就是结构体变量的拷贝。 每一层协议都是这么"贴"上去的。
三、sk_buff:一根 data 指针玩转封装与解包
任意时刻,操作系统内部存在多个报文在飞。要对它们进行管理,就轮到那句老话——先描述,后组织:
struct sk_buff {
// ... 四个核心字段:head / data / tail / end
// 指向的缓冲区:[head ... data ... tail ... end]
};
- 报文一来(比如数据链路层来到网络层),网络层为报文开一个 sk_buff 结构体对它进行描述;
- 对报文的管理,就转换为对数据结构 sk_buff 的增删查改——报文管理 ↔ sk_buff 增删查改;
- 内核会让编译期不对传输的数据做 padding 字节对齐,让数据紧密相连——方便内核操作和指针指向。
封装 = data 指针向下压栈
四个字段的舞蹈:
- tail 刚开始比 end 多出尾空间字节数(预留向上封装的空间);
- tail 和 data 重合(有效载荷夹在中间);
- head 等 data 语义完毕再赋值首空间。
每经过一层协议,data 指针依据报头长度(不对齐的字节数)向下移动——报头空间就这样被"让"出来:
应用层数据: [ data(你好) ] data 指向载荷头
加 UDP 报头: [UDP头][ data(你好) ] data -= 8
加 IP 报头: [IP头][UDP头][data(你好)] data -= 20
封装/解包的本质,就是 struct sk_buff 里 data 指针的移动——像压栈一样逐层进、逐层出。
四、file → socket → sock:网络文件的三层包装
应用层 read 一个网络 fd 时,数据怎么上来的?这就要看 socket 结构:
- 调
socket()时,内核创建一个 socket 结构体,file 的private_data*指针指向它——这就是 socket 凭什么认它是网络文件的依据; - socket 结构体里又有一个字段指向 sock 结构体——而实际挂的是 inet_sock / udp_sock / tcp_sock,用的时候强转指针即可;
- 排布遵循嵌套:sock → inet_sock → inet_connection_sock——最外面创建的是具体协议的 sock。
这是 C 语言实现多态的基础:struct sock 是基类,嵌套包含 + 指针强转,让同一个 file->f_op 接口能适配普通文件和网络文件两套操作函数表——file 结构体里的 f_op 指针指向不同的函数指针表,普通 file 和网络文件加载不同的表。
最前面的 sock 里挂着 struct sk_buff 队列!到这里全打通了:
1 个 file,2 个 sk_buff 队列(接收队列+发送队列)——文件操作、网络、sk_buff 三章合流。
五、数据到底怎么上来的:硬中断 → 软中断 → read 返回
最核心的一环,网卡来数据了:
- 硬中断(中断向量表、硬件信号触发)——硬中断函数把网卡数据给到管理队列(file 的双队列);
- 硬中断快结束时修改一个值,操作系统 if 到了,于是执行软中断(特殊 CPU 指令集触发)代码,进行压栈;
- 软中断 while 判断:file 的双队列里有没有数据?有就继续处理,直到数据返回给用户态函数参数所在的寄存器——read 返回,"你好"到手。
而"没数据"时呢?read 就阻塞——进程挂到 socket 里阻塞队列字段上等待(这解释了网络读写为什么天然阻塞)。
补一句大的世界观:操作系统本身是由大量中断构成的!进程的执行时机,本质是调度器什么时候把 CPU 给它;而调度器代码的执行是晶振引起、被动的——时钟中断驱动一切。想把网络理解透,就要理解调度机制、软硬中断这些操作系统本质。
六、UDP 的定位:不可靠,不是不可用
不可靠不是不可用! 相较 TCP 它更加简单:没有连接、没有确认、没有重传,8 字节报头 + 64K 上限。
它的地位恰恰来自这份"轻"——实时性要求高、丢了就丢了的场景(直播、DNS、心跳),UDP 才是正确答案。
七、守护进程化:让网络服务"自成会话"
视角切回用户态。为什么 ./exe 起的服务,你一断开 xshell 它就没了?这要从前台/后台、进程组、会话说起。
前后台与进程组
- 区分前后台进程的核心:键盘输入的获取——前台进程组才能读键盘;
- Ctrl C 是向前台进程组发信号;前台进程组只能有一个,后台进程组可以有多个;
- 进程组:为了支持管道而诞生——
A | B | C三个进程协同作战,暂停/发信号要一次管一窝,所以打成一个组;组的第一个进程叫组长; - 作业:bash 眼里的一项任务(用户视角),作业要由进程组完成;
jobs看后台作业,fg 2提到前台,命令后加&直接后台启动。
会话(sid):登录的本质
- 会话 = 建立一个 Linux 本地的进程组大组;xshell 是客户端,和远端建 TCP 连接、操作远端进程;终端文件是一种 fd,是向 Windows 传输的网络文件;
- 会话的初衷:多个用户用同一台服务器、相互隔离独立,提升服务器利用效率;
- 但它带来一个问题:会话结束,会话里的进程有可能被关掉!
这就和网络服务的需求正面冲突了——网络服务要求长部署、时效性,而进程组和会话恰恰会把它带走。所以:
守护进程的本质:让网络服务自成会话!成功调用了 setsid 函数的进程,就叫做守护进程。
组长为什么不能 setsid
进程组组长不能 setsid()。原因:组长进程的 pgid = 自己的 pid,如果它 setsid 成功,新守护进程会"sid 变了、pgid 却还是原组 pgid"——不属于同一会话却属于同一组,藕断丝连。你向原进程组发信号,会把守护进程一起整死。
所以守护进程的标准姿势:fork 出子进程(必然不是组长),父进程退出,子进程 setsid()——从此 sid、pgid 都是自己,孤家寡人,谁也牵连不了。
收尾:daemon 函数
为了守护进程化,系统给了现成的 daemon 函数,两个参数各有含义:
- 第一个参数控制要不要更改工作目录——为什么建议改?要是工作目录在某个挂载分区上,该分区/文件系统被删除,服务就毁了;
- 第二个参数控制要不要把标准输入输出重定向到
/dev/null——守护进程没有终端了,日志该走日志文件,别往终端怼。
最后留一个引子:为什么一个端口号用完释放后,短时间内不能再次 bind?——这是 TCP 协议的特性(TIME_WAIT),但现实世界需要我们立即重启服务器。怎么破?答案在下一段 TCP 底层的笔记里。
总结
- 端口 16 位由报头决定;四元组定位互联网唯一进程;UDP 报头定长 8 字节,2 字节长度 → 64K 上限;
- 内核看报文是结构体(指针一压直接识别),应用层看到的是字符串——封装的本质是结构体变量的拷贝;
- sk_buff 四字段(head/data/tail/end):报文管理 = sk_buff 增删查改;封装/解包 = data 指针的压栈移动;
- file → socket(private_data) → sock 三层包装,嵌套结构体 + 强转指针 + f_op 函数表 = C 语言多态;1 个 file 两条 sk_buff 队列;
- 数据上行靠硬中断(硬件信号)→ 软中断(特殊指令)→ while 处理 → 返回用户态;操作系统本身由大量中断构成,调度由晶振驱动;
- UDP 不可靠不是不可用,轻是它的价值;
- 会话/进程组/作业为隔离与协同而生,但会话一断服务就死——所以网络服务必须 setsid 自成会话(组长不能 setsid,fork 后子进程再调);daemon 函数管工作目录和 /dev/null 重定向;TIME_WAIT 的坑留给 TCP 章。
下一篇:TCP 的底层与三次握手——把"连接"这个词拆开看。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)