问题

Linux中本机和本机Socket通信会走网卡吗?

回答
在Linux系统中,本机(localhost)和本机(localhost)之间的Socket通信,也就是通常所说的本地回环(Loopback)通信,是不走物理网卡的。这是一个非常重要的概念,理解它能帮助我们更清晰地认识网络通信的底层机制。

让我们来详细剖析一下这个过程:

1. 本地回环接口:`lo`

Linux系统启动时,会虚拟地创建一个名为 `lo` 的网络接口。这个接口是专门为了处理本机内部的网络通信而存在的。它有一个固定的IP地址:`127.0.0.1`,以及一个与之关联的主机名:`localhost`。

为什么要有 `lo` 接口?想象一下,如果所有本机内部的通信都必须经过物理网卡,那将是多么低效且冗余的事情。物理网卡是设计用来与其他设备进行物理连接和数据交换的,它涉及电信号的发送、接收、MAC地址处理、ARP协议等等一系列复杂的物理层和数据链路层的操作。而本机内部的通信,根本不需要这些。`lo` 接口就像一个绕过物理硬件的捷径,直接在内核的网络协议栈内部完成数据传输。

2. Socket通信的流程(以TCP为例):

我们以一个简单的客户端服务器TCP通信为例来理解这个过程:

服务器端:
服务器程序调用 `socket()` 函数创建一个TCP Socket。
然后,它调用 `bind()` 函数将这个Socket绑定到一个本地地址和端口。通常情况下,服务器会绑定到 `127.0.0.1` (或 `localhost`)以及一个特定的端口号(例如 8080)。
接着,调用 `listen()` 函数开始监听传入的连接。
最后,调用 `accept()` 函数等待客户端的连接请求。

客户端端:
客户端程序也调用 `socket()` 函数创建一个TCP Socket。
然后,它调用 `connect()` 函数尝试连接到服务器。在 `connect()` 函数中,客户端会指定服务器的IP地址(`127.0.0.1`)和端口号(8080)。

3. 数据如何流动(关键点):

现在,当客户端调用 `connect()` 并指定 `127.0.0.1` 时,会发生什么?

内核拦截: Linux内核的网络协议栈会首先检查目标IP地址。当它发现目标IP是 `127.0.0.1` 时,它会立即识别出这是一个本地回环地址。
绕过网卡驱动: 内核不会将这个数据包发送到任何物理网卡驱动程序。它不会经过网卡硬件,更不会有电信号的产生和传输。
直接在协议栈内部传递: 内核会将数据包(或者更准确地说,Socket缓冲区中的数据)直接从发起连接(客户端)的Socket,“推”到正在监听的服务器Socket上。这个过程是在内核的内存空间中完成的。
TCP/IP协议栈处理: 在这个“推”的过程中,TCP/IP协议栈仍然会执行它应有的逻辑,例如建立TCP三次握手(SYN, SYNACK, ACK),数据分段、序列号、确认应答等。这些协议的处理仍然是存在的,只是数据在内核内部完成了流转,而无需涉及物理介质。
数据到达服务器: 当三次握手完成后,服务器的 `accept()` 函数就会返回一个新的Socket,表示连接已经建立。客户端发送的数据也同样是在内核内部通过协议栈传递给服务器Socket的缓冲区,然后被服务器的 `read()` 或 `recv()` 函数读取。

4. UDP通信的类似逻辑:

对于UDP(用户数据报协议)通信,流程也类似。当客户端发送一个UDP数据报到 `127.0.0.1` 时,内核同样会识别出是本地回环通信,并直接将数据报在协议栈内部传递给目标端口上的UDP Socket,而无需经过网卡。UDP本身是无连接的,所以没有TCP那样的三次握手过程,数据传递更直接。

5. 为什么不走网卡更高效?

避免硬件开销: 不需要物理信号的调制解调、信号传输、收发器工作等硬件层面的操作。
避免驱动层开销: 无需网卡驱动程序的处理,如DMA(直接内存访问)的设置、中断的处理等。
减少上下文切换: 物理网卡通信通常涉及用户空间和内核空间的多次上下文切换。本地回环通信可以极大地减少这种切换的次数。
避免 MAC 地址和 ARP: 无需处理MAC地址的封装、解封装,也无需进行ARP(地址解析协议)来查找目标MAC地址。

总结:

Linux本机(localhost)和本机(localhost)之间的Socket通信,之所以不走网卡,是因为Linux内核识别出目标IP地址是本地回环地址(`127.0.0.1`)后,会直接在内核的网络协议栈内部完成数据包的路由和传递。它利用的是一个虚拟的本地回环接口(`lo`),将数据从一个Socket的发送缓冲区直接传输到另一个Socket的接收缓冲区,跳过了所有与物理网卡硬件和驱动程序相关的操作。这种设计极大地提高了本机内部进程间通信的效率和性能。

网友意见

user avatar

这个问题非常不错,由于side car 模式的兴起,感觉本机网络 IO 用的越来越多了,所以我特地把该问题挖出来答一答。

不过我想把这个问题再丰富丰富,讨论起来更带劲!

  • 127.0.0.1 本机网络 IO 需要经过网卡吗?
  • 数据包在内核中是个什么走向,和外网发送相比流程上有啥差别?
  • 用本机 ip(例如192.168.x.x) 和用 127.0.0.1 性能上有差别吗?

这里先直接把结论抛出来。

  • 127.0.0.1 本机网络 IO 不经过网卡
  • 本机网络 IO 过程除了不过网卡,其它流程如内核协议栈还都得走。
  • 用本机 ip(例如192.168.x.x) 和用 127.0.0.1 性能没有大差别

内容来源于本人公-众-号: 开发内功修炼, 欢迎关注!

另外我把我对网络是如何收包的,如何使用 CPU,如何使用内存的对于内存的都深度分析了一下,还增加了一些性能优化建议和前沿技术展望等,最终汇聚出了这本《理解了实现再谈网络性能》。在此无私分享给大家。

下载链接传送门:《理解了实现再谈网络性能》


好了,继续讨论今天的问题!

一、跨机网路通信过程

在开始讲述本机通信过程之前,我们还是先回顾一下跨机网络通信。

1.1 跨机数据发送

从 send 系统调用开始,直到网卡把数据发送出去,整体流程如下:

在这幅图中,我们看到用户数据被拷贝到内核态,然后经过协议栈处理后进入到了 RingBuffer 中。随后网卡驱动真正将数据发送了出去。当发送完成的时候,是通过硬中断来通知 CPU,然后清理 RingBuffer。

不过上面这幅图并没有很好地把内核组件和源码展示出来,我们再从代码的视角看一遍。

等网络发送完毕之后。网卡在发送完毕的时候,会给 CPU 发送一个硬中断来通知 CPU。收到这个硬中断后会释放 RingBuffer 中使用的内存。

更详细的分析过程参见:

1.2 跨机数据接收

当数据包到达另外一台机器的时候,Linux 数据包的接收过程开始了。

当网卡收到数据以后,向 CPU 发起一个中断,以通知 CPU 有数据到达。当CPU收到中断请求后,会去调用网络驱动注册的中断处理函数,触发软中断。ksoftirqd 检测到有软中断请求到达,开始轮询收包,收到后交由各级协议栈处理。当协议栈处理完并把数据放到接收队列的之后,唤醒用户进程(假设是阻塞方式)。

我们再同样从内核组件和源码视角看一遍。

详细的接收过程参见这篇文章:图解Linux网络包接收过程

1.3 跨机网络通信汇总

二、本机发送过程

在第一节中,我们看到了跨机时整个网络发送过程(嫌第一节流程图不过瘾,想继续看源码了解细节的同学可以参考 拆解 Linux 网络包发送过程) 。

在本机网络 IO 的过程中,流程会有一些差别。为了突出重点,将不再介绍整体流程,而是只介绍和跨机逻辑不同的地方。有差异的地方总共有两个,分别是路由驱动程序

2.1 网络层路由

发送数据会进入协议栈到网络层的时候,网络层入口函数是 ip_queue_xmit。在网络层里会进行路由选择,路由选择完毕后,再设置一些 IP 头、进行一些 netfilter 的过滤后,将包交给邻居子系统。

对于本机网络 IO 来说,特殊之处在于在 local 路由表中就能找到路由项,对应的设备都将使用 loopback 网卡,也就是我们常见的 lo。

我们来详细看看路由网络层里这段路由相关工作过程。从网络层入口函数 ip_queue_xmit 看起。

       //file: net/ipv4/ip_output.c int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl) {  //检查 socket 中是否有缓存的路由表  rt = (struct rtable *)__sk_dst_check(sk, 0);  if (rt == NULL) {   //没有缓存则展开查找   //则查找路由项, 并缓存到 socket 中   rt = ip_route_output_ports(...);   sk_setup_caps(sk, &rt->dst);  }     

查找路由项的函数是 ip_route_output_ports,它又依次调用到 ip_route_output_flow、__ip_route_output_key、fib_lookup。调用过程省略掉,直接看 fib_lookup 的关键代码。

       //file:include/net/ip_fib.h static inline int fib_lookup(struct net *net, const struct flowi4 *flp,         struct fib_result *res) {  struct fib_table *table;   table = fib_get_table(net, RT_TABLE_LOCAL);  if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))   return 0;   table = fib_get_table(net, RT_TABLE_MAIN);  if (!fib_table_lookup(table, flp, res, FIB_LOOKUP_NOREF))   return 0;  return -ENETUNREACH; }     

在 fib_lookup 将会对 local 和 main 两个路由表展开查询,并且是先查 local 后查询 main。我们在 Linux 上使用命令名可以查看到这两个路由表, 这里只看 local 路由表(因为本机网络 IO 查询到这个表就终止了)。

       #ip route list table local local 10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1     

从上述结果可以看出,对于目的是 127.0.0.1 的路由在 local 路由表中就能够找到了。fib_lookup 工作完成,返回__ip_route_output_key 继续。

       //file: net/ipv4/route.c struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {  if (fib_lookup(net, fl4, &res)) {  }  if (res.type == RTN_LOCAL) {   dev_out = net->loopback_dev;   ...  }   rth = __mkroute_output(&res, fl4, orig_oif, dev_out, flags);  return rth; }     

对于是本机的网络请求,设备将全部都使用 net->loopback_dev,也就是 lo 虚拟网卡。

接下来的网络层仍然和跨机网络 IO 一样,最终会经过 ip_finish_output,最终进入到 邻居子系统的入口函数 dst_neigh_output 中。

本机网络 IO 需要进行 IP 分片吗?因为和正常的网络层处理过程一样会经过 ip_finish_output 函数。在这个函数中,如果 skb 大于 MTU 的话,仍然会进行分片。只不过 lo 的 MTU 比 Ethernet 要大很多。通过 ifconfig 命令就可以查到,普通网卡一般为 1500,而 lo 虚拟接口能有 65535。

在邻居子系统函数中经过处理,进入到网络设备子系统(入口函数是 dev_queue_xmit)。

2.2 本机 IP 路由

开篇我们提到的第三个问题的答案就在前面的网络层路由一小节中。但这个问题描述起来有点长,因此单独拉一小节出来。

问题:用本机 ip(例如192.168.x.x) 和用 127.0.0.1 性能上有差别吗?

前面看到选用哪个设备是路由相关函数 __ip_route_output_key 中确定的。

       //file: net/ipv4/route.c struct rtable *__ip_route_output_key(struct net *net, struct flowi4 *fl4) {  if (fib_lookup(net, fl4, &res)) {  }  if (res.type == RTN_LOCAL) {   dev_out = net->loopback_dev;   ...  }   rth = __mkroute_output(&res, fl4, orig_oif, dev_out, flags);  return rth; }     

这里会查询到 local 路由表。

       # ip route list table local local 10.162.*.* dev eth0  proto kernel  scope host  src 10.162.*.* local 127.0.0.1 dev lo  proto kernel  scope host  src 127.0.0.1     

很多人在看到这个路由表的时候就被它给迷惑了,以为上面 10.162. 真的会被路由到 eth0(其中 10.162.. 是我的本机局域网 IP,我把后面两段用 * 号隐藏起来了)。

但其实内核在初始化 local 路由表的时候,把 local 路由表里所有的路由项都设置成了 RTN_LOCAL,不仅仅只是 127.0.0.1。这个过程是在设置本机 ip 的时候,调用 fib_inetaddr_event 函数完成设置的。

       static int fib_inetaddr_event(struct notifier_block *this,   unsigned long event, void *ptr) {  switch (event) {  case NETDEV_UP:   fib_add_ifaddr(ifa);   break;  case NETDEV_DOWN:   fib_del_ifaddr(ifa, NULL);  //file:ipv4/fib_frontend.c void fib_add_ifaddr(struct in_ifaddr *ifa) {  fib_magic(RTM_NEWROUTE, RTN_LOCAL, addr, 32, prim); }     

所以即使本机 IP,不用 127.0.0.1,内核在路由项查找的时候判断类型是 RTN_LOCAL,仍然会使用 net->loopback_dev。也就是 lo 虚拟网卡。

为了稳妥起见,飞哥再抓包确认一下。开启两个控制台窗口,一个对 eth0 设备进行抓包。因为局域网内会有大量的网络请求,为了方便过滤,这里使用一个特殊的端口号 8888。如果这个端口号在你的机器上占用了,那需要再换一个。

       #tcpdump -i eth0 port 8888     

另外一个窗口使用 telnet 对本机 IP 端口发出几条网络请求。

       #telnet 10.162.*.* 8888 Trying 10.162.129.56... telnet: connect to address 10.162.129.56: Connection refused     

这时候切回第一个控制台,发现啥反应都没有。说明包根本就没有过 eth0 这个设备。

再把设备换成 lo 再抓。当 telnet 发出网络请求以后,在 tcpdump 所在的窗口下看到了抓包结果。

       # tcpdump -i lo port 8888 tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes 08:22:31.956702 IP 10.162.*.*.62705 > 10.162.*.*.ddi-tcp-1: Flags [S], seq 678725385, win 43690, options [mss 65495,nop,wscale 8], length 0 08:22:31.956720 IP 10.162.*.*.ddi-tcp-1 > 10.162.*.*.62705: Flags [R.], seq 0, ack 678725386, win 0, length 0     

2.3 网络设备子系统

网络设备子系统的入口函数是 dev_queue_xmit。简单回忆下之前讲述跨机发送过程的时候,对于真的有队列的物理设备,在该函数中进行了一系列复杂的排队等处理以后,才调用 dev_hard_start_xmit,从这个函数 再进入驱动程序来发送。在这个过程中,甚至还有可能会触发软中断来进行发送,流程如图:

但是对于启动状态的回环设备来说(q->enqueue 判断为 false),就简单多了。没有队列的问题,直接进入 dev_hard_start_xmit。接着中进入回环设备的“驱动”里的发送回调函数 loopback_xmit,将 skb “发送”出去。

我们来看下详细的过程,从 网络设备子系统的入口 dev_queue_xmit 看起。

       //file: net/core/dev.c int dev_queue_xmit(struct sk_buff *skb) {  q = rcu_dereference_bh(txq->qdisc);  if (q->enqueue) {//回环设备这里为 false   rc = __dev_xmit_skb(skb, q, dev, txq);   goto out;  }   //开始回环设备处理  if (dev->flags & IFF_UP) {   dev_hard_start_xmit(skb, dev, txq, ...);   ...  } }     

在 dev_hard_start_xmit 中还是将调用设备驱动的操作函数。

       //file: net/core/dev.c int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,    struct netdev_queue *txq) {  //获取设备驱动的回调函数集合 ops  const struct net_device_ops *ops = dev->netdev_ops;   //调用驱动的 ndo_start_xmit 来进行发送  rc = ops->ndo_start_xmit(skb, dev);  ... }     

2.3 “驱动”程序

对于真实的 igb 网卡来说,它的驱动代码都在 drivers/net/ethernet/intel/igb/igb_main.c 文件里。顺着这个路子,我找到了 loopback 设备的“驱动”代码位置:drivers/net/loopback.c。 在 drivers/net/loopback.c

       //file:drivers/net/loopback.c static const struct net_device_ops loopback_ops = {  .ndo_init      = loopback_dev_init,  .ndo_start_xmit= loopback_xmit,  .ndo_get_stats64 = loopback_get_stats64, };     

所以对 dev_hard_start_xmit 调用实际上执行的是 loopback “驱动” 里的 loopback_xmit。为什么我把“驱动”加个引号呢,因为 loopback 是一个纯软件性质的虚拟接口,并没有真正意义上的驱动。

       //file:drivers/net/loopback.c static netdev_tx_t loopback_xmit(struct sk_buff *skb,      struct net_device *dev) {  //剥离掉和原 socket 的联系  skb_orphan(skb);   //调用netif_rx  if (likely(netif_rx(skb) == NET_RX_SUCCESS)) {  } }     

在 skb_orphan 中先是把 skb 上的 socket 指针去掉了(剥离了出来)。

注意,在本机网络 IO 发送的过程中,传输层下面的 skb 就不需要释放了,直接给接收方传过去就行了。总算是省了一点点开销。不过可惜传输层的 skb 同样节约不了,还是得频繁地申请和释放。

接着调用 netif_rx,在该方法中 中最终会执行到 enqueue_to_backlog 中(netif_rx -> netif_rx_internal -> enqueue_to_backlog)。

       //file: net/core/dev.c static int enqueue_to_backlog(struct sk_buff *skb, int cpu,          unsigned int *qtail) {  sd = &per_cpu(softnet_data, cpu);   ...  __skb_queue_tail(&sd->input_pkt_queue, skb);   ...  ____napi_schedule(sd, &sd->backlog);       

在 enqueue_to_backlog 把要发送的 skb 插入 softnet_data->input_pkt_queue 队列中并调用 ____napi_schedule 来触发软中断。

       //file:net/core/dev.c static inline void ____napi_schedule(struct softnet_data *sd,          struct napi_struct *napi) {  list_add_tail(&napi->poll_list, &sd->poll_list);  __raise_softirq_irqoff(NET_RX_SOFTIRQ); }     

只有触发完软中断,发送过程就算是完成了。

三、本机接收过程

在跨机的网络包的接收过程中,需要经过硬中断,然后才能触发软中断。而在本机的网络 IO 过程中,由于并不真的过网卡,所以网卡实际传输,硬中断就都省去了。直接从软中断开始,经过 process_backlog 后送进协议栈,大体过程如图。

接下来我们再看更详细一点的过程。
在软中断被触发以后,会进入到 NET_RX_SOFTIRQ 对应的处理方法 net_rx_action 中(至于细节参见 图解Linux网络包接收过程 一文中的 3.2 小节)。

       //file: net/core/dev.c static void net_rx_action(struct softirq_action *h){  while (!list_empty(&sd->poll_list)) {   work = n->poll(n, weight);  } }     

我们还记得对于 igb 网卡来说,poll 实际调用的是 igb_poll 函数。那么 loopback 网卡的 poll 函数是谁呢?由于poll_list 里面是 struct softnet_data 对象,我们在 net_dev_init 中找到了蛛丝马迹。

       //file:net/core/dev.c static int __init net_dev_init(void) {  for_each_possible_cpu(i) {   sd->backlog.poll = process_backlog;  } }     

原来struct softnet_data 默认的 poll 在初始化的时候设置成了 process_backlog 函数,来看看它都干了啥。

       static int process_backlog(struct napi_struct *napi, int quota) {  while(){   while ((skb = __skb_dequeue(&sd->process_queue))) {    __netif_receive_skb(skb);   }    //skb_queue_splice_tail_init()函数用于将链表a连接到链表b上,   //形成一个新的链表b,并将原来a的头变成空链表。   qlen = skb_queue_len(&sd->input_pkt_queue);   if (qlen)    skb_queue_splice_tail_init(&sd->input_pkt_queue,          &sd->process_queue);     } }     

这次先看对 skb_queue_splice_tail_init 的调用。源码就不看了,直接说它的作用是把 sd->input_pkt_queue 里的 skb 链到 sd->process_queue 链表上去。

然后再看 __skb_dequeue, __skb_dequeue 是从 sd->process_queue 上取下来包来处理。这样和前面发送过程的结尾处就对上了,发送过程是把包放到了 input_pkt_queue 队列里。

最后调用 __netif_receive_skb 将数据送往协议栈。在此之后的调用过程就和跨机网络 IO 又一致了。

送往协议栈的调用链是 __netif_receive_skb => __netif_receive_skb_core => deliver_skb 后 将数据包送入到 ip_rcv 中(详情参见图解Linux网络包接收过程 一文中的 3.3 小节)。

网络再往后依次是传输层,最后唤醒用户进程。

四、本机网络 IO 总结

我们来总结一下本机网络 IO 的内核执行流程。

回想下跨机网络 IO 的流程是


我们现在可以回顾下开篇的三个问题啦。

1)127.0.0.1 本机网络 IO 需要经过网卡吗? 通过本文的叙述,我们确定地得出结论,不需要经过网卡。即使了把网卡拔了本机网络是否还可以正常使用的。

2)数据包在内核中是个什么走向,和外网发送相比流程上有啥差别? 总的来说,本机网络 IO 和跨机 IO 比较起来,确实是节约了驱动上的一些开销。发送数据不需要进 RingBuffer 的驱动队列,直接把 skb 传给接收协议栈(经过软中断)。但是在内核其它组件上,可是一点都没少,系统调用、协议栈(传输层、网络层等)、设备子系统整个走了一个遍。连“驱动”程序都走了(虽然对于回环设备来说只是一个纯软件的虚拟出来的东东)。所以即使是本机网络 IO,切忌误以为没啥开销就滥用。

3)用本机 ip(例如192.168.x.x) 和用 127.0.0.1 性能上有差别吗? 很多人的直觉是走网卡,但正确结论是和 127.0.0.1 没有差别,都是走虚拟的环回设备 lo。
这是因为内核在设置 ip 的时候,把所有的本机 ip 都初始化 local 路由表里了,而且类型写死 RTN_LOCAL。在后面的路由项选择的时候发现类型是 RTN_LOCAL 就会选择 lo 了。还不信的话你也动手抓包试试!

最后再提一下,业界有公司基于 ebpf 的 sockmap 和 sk redirect 功能研发了自己的 sockops 组件,用来加速 istio 架构中 sidecar 代理和本地进程之间的通信。通过引入 BPF,才算是绕开了内核协议栈的开销,原理如下。


飞哥写了一本电子书。这本电子书是对网络性能进行拆解,把性能拆分为三个角度:CPU 开销、内存开销等。

具体到某个角度比如 CPU,那我需要给自己解释清楚网络包是怎么从网卡到内核中的,内核又是通过哪些方式通知进程的。只有理解清楚了这些才能真正把握网络对 CPU 的消耗。

对于内存角度也是一样,只有理解了内核是如何使用内存,甚至需要哪些内核对象都搞清楚,也才能真正理解一条 TCP 连接的内存开销。

除此之外我还增加了一些性能优化建议和前沿技术展望等,最终汇聚出了这本《理解了实现再谈网络性能》。在此无私分享给大家。

下载链接传送门:《理解了实现再谈网络性能》

另外飞哥经常会收到读者的私信,询问可否推荐一些书继续深入学习内功。所以我干脆就写了篇文章。把能搜集到的电子版也帮大家汇总了一下,取需!

答读者问,能否推荐几本有价值的参考书(含下载地址)

Github: https://github.com/yanfeizhang/


------------------------------ 华丽的分割线 ----------------------------------------

2021-12-22 日追更

在上次回答完后,有读者在评论区里希望飞哥能再分析一下 Unix Domain Socket。最近终于抽空把这个也深入研究了一下。

今天我们将分析 Unix Domain Socket 的连接建立过程、数据发送过程等内部工作原理。你将理解为什么这种方式的性能比 127.0.0.1 要好很多。最后我们还给出了实际的性能测试对比数据。

相信你已经迫不及待了,别着急,让我们一一展开细说!

一、连接过程

总的来说,基于 UDS 的连接过程比 inet 的 socket 连接过程要简单多了。客户端先创建一个自己用的 socket,然后调用 connect 来和服务器建立连接。

在 connect 的时候,会申请一个新 socket 给 server 端将来使用,和自己的 socket 建立好连接关系以后,就放到服务器正在监听的 socket 的接收队列中。 这个时候,服务器端通过 accept 就能获取到和客户端配好对的新 socket 了。

总的 UDS 的连接建立流程如下图。

内核源码中最重要的逻辑在 connect 函数中,我们来简单展开看一下。 unix 协议族中定义了这类 socket 的所有方法,它位于 net/unix/af_unix.c 中。

       //file: net/unix/af_unix.c static const struct proto_ops unix_stream_ops = {  .family = PF_UNIX,  .owner = THIS_MODULE,  .bind =  unix_bind,  .connect = unix_stream_connect,  .socketpair = unix_socketpair,  .listen = unix_listen,  ... };     

我们找到 connect 函数的具体实现,unix_stream_connect。

       //file: net/unix/af_unix.c static int unix_stream_connect(struct socket *sock, struct sockaddr *uaddr,           int addr_len, int flags) {  struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;   ...   // 1. 为服务器侧申请一个新的 socket 对象  newsk = unix_create1(sock_net(sk), NULL);   // 2. 申请一个 skb,并关联上 newsk  skb = sock_wmalloc(newsk, 1, 0, GFP_KERNEL);  ...   // 3. 建立两个 sock 对象之间的连接  unix_peer(newsk) = sk;  newsk->sk_state  = TCP_ESTABLISHED;  newsk->sk_type  = sk->sk_type;  ...  sk->sk_state = TCP_ESTABLISHED;  unix_peer(sk) = newsk;   // 4. 把连接中的一头(新 socket)放到服务器接收队列中  __skb_queue_tail(&other->sk_receive_queue, skb); }     

主要的连接操作都是在这个函数中完成的。和我们平常所见的 TCP 连接建立过程,这个连接过程简直是太简单了。没有三次握手,也没有全连接队列、半连接队列,更没有啥超时重传。

直接就是将两个 socket 结构体中的指针互相指向对方就行了。就是 unix_peer(newsk) = sk 和 unix_peer(sk) = newsk​ 这两句。

                //file: net/unix/af_unix.c                   #define unix_peer(sk) (unix_sk(sk)->peer)            

当关联关系建立好之后,通过 __skb_queue_tail 将 skb 放到服务器的接收队列中。注意这里的 skb 里保存着新 socket 的指针,因为服务进程通过 accept 取出这个 skb 的时候,就能获取到和客户进程中 socket 建立好连接关系的另一个 socket。

怎么样,UDS 的连接建立过程是不是很简单!?

二、发送过程

看完了连接建立过程,我们再来看看基于 UDS 的数据的收发。这个收发过程一样也是非常的简单。发送方是直接将数据写到接收方的接收队列里的。

我们从 send 函数来看起。send 系统调用的源码位于文件 net/socket.c 中。在这个系统调用里,内部其实真正使用的是 sendto 系统调用。它只干了两件简单的事情,

第一是在内核中把真正的 socket 找出来,在这个对象里记录着各种协议栈的函数地址。 第二是构造一个 struct msghdr 对象,把用户传入的数据,比如 buffer地址、数据长度啥的,统统都装进去. 剩下的事情就交给下一层,协议栈里的函数 inet_sendmsg 了,其中 inet_sendmsg 函数的地址是通过 socket 内核对象里的 ops 成员找到的。大致流程如图。

在进入到协议栈 inet_sendmsg 以后,内核接着会找到 socket 上的具体协议发送函数。对于 Unix Domain Socket 来说,那就是 unix_stream_sendmsg。 我们来看一下这个函数

       //file: static int unix_stream_sendmsg(struct kiocb *kiocb, struct socket *sock,           struct msghdr *msg, size_t len) {  // 1.申请一块缓存区  skb = sock_alloc_send_skb(sk, size, msg->msg_flags&MSG_DONTWAIT,       &err);   // 2.拷贝用户数据到内核缓存区  err = memcpy_fromiovec(skb_put(skb, size), msg->msg_iov, size);   // 3. 查找socket peer  struct sock *other = NULL;  other = unix_peer(sk);   // 4.直接把 skb放到对端的接收队列中  skb_queue_tail(&other->sk_receive_queue, skb);   // 5.发送完毕回调  other->sk_data_ready(other, size); }     

和复杂的 TCP 发送接收过程相比,这里的发送逻辑简单简单到令人发指。申请一块内存(skb),把数据拷贝进去。根据 socket 对象找到另一端,直接把 skb 给放到对端的接收队列里了

接收函数主题是 unix_stream_recvmsg,这个函数中只需要访问它自己的接收队列就行了,源码就不展示了。所以在本机网络 IO 场景里,基于 Unix Domain Socket 的服务性能上肯定要好一些的。

三、性能对比

为了验证 Unix Domain Socket 到底比基于 127.0.0.1 的性能好多少,我做了一个性能测试。 在网络性能对比测试,最重要的两个指标是延迟和吞吐。我从 Github 上找了个好用的测试源码:github.com/rigtorp/ipc-。 我的测试环境是一台 4 核 CPU,8G 内存的 KVM 虚机。

在延迟指标上,对比结果如下图。

可见在小包(100 字节)的情况下,UDS 方法的“网络” IO 平均延迟只有 2707 纳秒,而基于 TCP(访问 127.0.0.1)的方式下延迟高达 5690 纳秒。耗时整整是前者的两倍。

在包体达到 100 KB 以后,UDS 方法延迟 24 微秒左右(1 微秒等于 1000 纳秒),TCP 是 32 微秒,仍然高一截。这里低于 2 倍的关系了,是因为当包足够大的时候,网络协议栈上的开销就显得没那么明显了。

再来看看吞吐效果对比。

在小包的情况下,带宽指标可以达到 854 M,而基于 TCP 的 IO 方式下只有 386。

四、总结

本文分析了基于 Unix Domain Socket 的连接创建、以及数据收发过程。其中数据收发的工作过程如下图。

相对比本机网络 IO 通信过程上,它的工作过程要清爽许多。其中 127.0.0.1 工作过程如下图。

我们也对比了 UDP 和 TCP 两种方式下的延迟和性能指标。在包体不大于 1KB 的时候,UDS 的性能大约是 TCP 的两倍多。所以,在本机网络 IO 的场景下,如果对性能敏感,可考虑使用 Unix Domain Socket。

看到这里留个赞再走呗!

也欢迎关注飞哥的公众号:开发内功修炼

user avatar

没必要翻源代码,没必要画图,也没必要抓包,

看看路由表干什么

路由表控制包走卡还是回环。

这个不是特定问题,参考通讯行业人人桌面的tcp/ip协议详解即可

user avatar

这个问题其实还挺复杂的,严格来说不是一句“会”或者“不会”能说清楚的。

在linux当中,socket包括两类:使用网络的socket,称为Berkeley socket;不使用网络的socket,称为Unix domain socket。两者API结构很像,所以容易被混淆,但是其实不是一个东西。

Unix domain socket是为单机上跨进程通信,也就是IPC设计的。通信数据直接通过kernel在进程间交换,与网卡毫无关联。

Berkeley socket则使用网络协议栈,也就是遵从OSI那个七层模型。其中,网卡是处在最下面的物理层,在linux当中,这一层除了物理网卡,还包括lo(loopback)设备,甚至还有一些软件虚拟NIC,如ipv4到ipv6的网桥,虚拟机的虚拟网卡,或者当单个网卡要绑定多个IP地址时出现的虚拟网卡。

在使用Berkeley socket的时候,和本机通信是否走网卡,取决于通信的两端,也就是两个socket的网络地址绑定情况。通常情况下,无论是绑定127.0.0.1,还是绑定其它分配给本机的地址,都会对应路由表当中的lo设备,通信数据通过kernel直接传输,不会使用到物理网卡。

但是如果我们修改路由表,让其不要使用lo设备而是使用物理网卡,则数据就会出现在网线上。

类似的话题

  • 回答
    在Linux系统中,本机(localhost)和本机(localhost)之间的Socket通信,也就是通常所说的本地回环(Loopback)通信,是不走物理网卡的。这是一个非常重要的概念,理解它能帮助我们更清晰地认识网络通信的底层机制。让我们来详细剖析一下这个过程:1. 本地回环接口:`lo`Li.............
  • 回答
    在 Linux 系统中,“一切皆文件”这个说法,对于初学者来说,可能听起来有些抽象,甚至让人觉得不可思议。但它却是理解 Linux 设计哲学和强大之处的核心。简单来说,这句话指的是,Linux 系统将各种资源,包括硬件设备、进程信息、网络连接,甚至是系统配置和内核信息,都抽象成了文件(或者说以文件的.............
  • 回答
    在 Linux 系统中,可执行文件的扩展名并不是一个强制性的要求。与 Windows 系统不同,Linux 主要依靠文件的权限位来判断一个文件是否可以被执行。也就是说,即使一个文件没有任何扩展名,只要它拥有执行权限,就可以被系统当作可执行文件来运行。然而,在实际的开发和管理过程中,为了方便识别、明确.............
  • 回答
    .......
  • 回答
    在Linux系统中,卸载Python后,系统是否能正常运行取决于以下因素:系统本身是否依赖Python、Python在系统中的角色、以及用户自定义的软件或服务是否依赖Python。以下是详细分析: 1. 系统核心是否依赖Python?Linux系统的核心组件(如内核、系统调用、设备驱动等)不依赖Py.............
  • 回答
    在 Linux 系统中,使用 C 语言判断 `yum` 源是否配置妥当,并不是直接调用一个 C 函数就能完成的事情,因为 `yum` 的配置和操作是一个相对复杂的系统级任务,涉及到文件系统、网络通信、进程管理等多个层面。更准确地说,我们通常是通过 模拟 `yum` 的一些基本行为 或者 检查 `yu.............
  • 回答
    在 Linux 内核中,为多线程(更准确地说,为进程中的线程)分配和管理栈空间是一个至关重要的环节,它直接关系到程序的执行稳定性、资源利用率以及并发安全性。理解这一模型,需要我们深入到用户空间和内核空间两个层面,以及它们之间的交互。核心概念:栈(Stack)首先,让我们明确栈是什么。栈是一种后进先出.............
  • 回答
    在Linux主机中增加一块内存条后,物理地址的扩展是一个相对底层和自动化的过程,主要依赖于硬件(内存控制器、CPU)和操作系统内核的协同工作。这并非一个需要用户手动干预的“步骤”,而是系统识别和利用新增内存的内在机制。下面我将尽量详细地解释这个过程,尽量剥离掉任何可能让人觉得是AI生成的痕迹,用一种.............
  • 回答
    要说 Linux 发行版中哪个包管理器“更强”,其实是个挺有意思的问题,因为它涉及到很多不同的维度去衡量。没有一个绝对的答案说“A 就是比 B 强”,更多的是它们在设计理念、功能侧重和使用体验上的不同,造就了各自的优势。如果你是 Linux 新手,可能会觉得所有包管理器都差不多,输入个 `insta.............
  • 回答
    .......
  • 回答
    在中国中小学计算机课堂中,推广 Linux 系统而不用 Windows,这绝对是一个值得深入探讨的问题,而且,答案是:可能性是存在的,但挑战也相当巨大。要详细解读这一点,我们需要从几个层面去分析。一、 推广 Linux 的潜在优势与吸引力首先,为什么会有人想要在中小学阶段推广 Linux?这背后肯定.............
  • 回答
    Linux Kernel 4.9 中引入的 BBR (Bottleneck Bandwidth and Roundtrip propagation time) 算法代表了 TCP 拥塞控制领域的一个重要进步。与之前广泛使用的算法(如 Cubic、Reno、NewReno)相比,BBR 具有以下显著优.............
  • 回答
    这个问题其实触及了嵌入式Linux系统启动过程中的一些核心概念,涉及到CPU的启动流程、内存映射以及内核映像的加载。我们来详细梳理一下。首先,我们要理解“内存中运行地址0x30008000到内存起始运行地址0x30000000”这个描述。这里的两个地址,0x30008000和0x30000000,显.............
  • 回答
    .......
  • 回答
    .......
  • 回答
    要说 Linux 的核心思想,那得从它诞生的时代背景聊起。那时候,操作系统还是一个比较封闭且昂贵的东西,主要是大型机和小型机的天下。普通人想要玩点啥,要么得花大价钱,要么只能玩一些非常简陋的系统。这时候,一个叫 Linus Torvalds 的芬兰大学生,出于对现有操作系统的“不满”和对学习计算机原.............
  • 回答
    当Linux系统更新后无法启动时,确实会让人感到焦虑和无助,但通过系统性排查和步骤操作,通常可以逐步解决问题。以下是详细的心理状态分析和应对步骤: 一、心情与心理状态1. 焦虑与着急:系统无法启动意味着无法进行常规操作,可能涉及重要数据丢失或服务中断,导致用户感到紧张。2. 无助感:如果对系统技术细.............
  • 回答
    Linux 系统确实具有“天生安全基因”,其整体安全性设计在操作系统层面具有显著优势,这源于其设计哲学、技术架构和开源生态的综合影响。以下从多个维度详细分析 Linux 的安全性特点及其优势: 1. 设计哲学:最小化、模块化与隔离性Linux 的设计哲学强调最小化攻击面和模块化架构,这些原则直接提升.............
  • 回答
    在Linux下进行Socket编程时,需要注意以下几个关键点,以确保程序的稳定性、安全性、性能和跨平台兼容性: 一、基础概念与步骤1. Socket类型与协议选择 TCP(面向连接):适合可靠数据传输,需通过三次握手建立连接。 UDP(无连接):适合低延迟场景,但可能丢失数据包。 .............
  • 回答
    Linux 之所以坚持使用宏内核(Monolithic Kernel)架构,主要源于其设计哲学、性能需求、开发历史以及对系统稳定性和可扩展性的追求。以下从多个角度详细分析这一选择的合理性: 1. 性能优势:减少上下文切换和系统调用开销 宏内核的直接性:在宏内核中,所有操作系统功能(如进程调度、设备驱.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有