- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章Linux fd 系列 — socket fd 是什么?由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
。
什么是 socket fd ?粗糙的来讲,就是网络 fd,比如我们最常见的 C/S 客户端服务端的编程模式,就是网络通信的一种方式。撇开底层和协议细节,网络通信和文件读写从接口上有本质区别吗?
其实没啥区别,不就是读过来和写过去嘛,简称 IO .
我们先看一下 socket fd 是什么样子的?随便找了个进程 。
这里我们看到 fd 7、8 都是一个 socket fd,名字:socket:[18892] 。
整数句柄后面一般会跟一些信息,用于帮助我们了解这个 fd 是什么。举个例子,如果是文件 fd,那么箭头后面一般是路径名称。现在拆解一下这个名字:
思考下,这个 inode 号,还能再哪里能看到呢?
在 proc 的 net 目录下,因为我这个是一个走 tcp 的服务端,所以我们看一下 /proc/net/tcp 文件。这个文件里面能看到所有的 tcp 连接的信息.
知识点又来了,/proc/net/tcp 这个文件记录了 tcp 连接的信息,这份信息是非常有用的。包含了 TCP 连接的地址(16进制显示),inode 的信息,连接的状态等等.
环境声明:
Linux 内核版本 4.19 。
为了方便,如果没特意说明协议,默认 TCP 协议; 。
socket 可能你还没反应过来,中文名:套接字 是不是更熟悉点。Linux 网络编程甚至可以叫做套接字编程.
有些概念你必须捋一捋 。我们思考几个小问题:
socket 跟 tcp/ip 有什么区别?
就不该把这两个东西放在一起比较讨论,就不是一个东西。tcp/ip 是网络协议栈,socket 是操作系统为了方便网络编程而设计出来的编程接口而已.
理论基础是各种网络协议,协议栈呀,啥的。但是如果你要进行网络编程,落到实处,对程序猿来讲就是 socket 编程.
对于网络的操作,由 socket 体现为 open -> read/write ->close 这样的编程模式,这个统一到文件的一种形式.
socket 的 open 就是 socket(int domain, int type, int protocol) ,和文件一样,都是获取一个句柄.
网络模型一般会对应到两种:
对应关系如下图(取自 Unix 套接字编程) 。
不同层次做不同的事情,不断的封装,不断的站在巨人的肩膀上,你将能做的更多.
今天,奇伢剖析的只聚焦在套接字这一层,这是程序猿摸得到的一层,位于所有网络协议之上的一层封装,网络编程又叫套接字编程,这并不是空穴来风.
套接字,是内核对贼复杂的网络协议栈的 API 封装,使得程序猿能够用极简的姿势进行网络编程。比如写一个基于 Tcp 的 C/S 的网络程序,需要用到啥?我们大概畅想下:
程序猿用着好简单!因为内核把事扛了.
上面我们提到了套接字,这是我们网络编程的主体,套接字由 socket() 系统调用创建,但你可知套接字其实可分为两种类型,监听套接字和普通套接字。而监听套接字是由 listen() 把 socket fd 转化而成.
对于监听套接字,不走数据流,只管理连接的建立。accept 将从全连接队列获取一个创建好的 socket( 3 次握手完成),对于监听套接字的可读事件就是全连接队列非空。对于监听套接字,我们只在乎可读事件.
普通套接字就是走数据流的,也就是网络 IO,针对普通套接字我们关注可读可写事件。在说 socket 的可读可写事件之前,我们先捋顺套接字的读写大概是什么样子吧.
套接字层是内核提供给程序员用来网络编程的,程序猿读写都是针对套接字而言,那么 write( socketfd, /* 参数 */) 和 read( socketfd, /* 参数 */) 都会发生什么呢?
也就是说,程序猿而言,是跟 socket 打交道,内核屏蔽了底层的细节.
那说回来 socket 的可读可写事件就很容易理解了.
socket fd 为什么能具备“文件”的语义,从而和 eventfd,ext2 fd 这样的句柄一样,统一提供对外 io 的样子?
核心就是:sockfs ,这也是个文件系统,只不过普通用户看不见,这是只由内核管理的文件系统,位于 vfs 之下,为了封装 socket 对上的文件语义.
其中最关键的是 sock_mnt 这个全局变量里面的超级块的操作表 sockfs_ops .
这个是每个文件系统的核心函数表,如上指明了 inode 的分配规则(这里又将体现依次结构体内嵌组合+类型强转的应用).
读者朋友还记得 inode 和 ext4_inode_info 的关系吗?在 Linux fd 究竟是什么?一文中有提到这个:
inode 是 vfs 抽象的适配所有文件系统的结构体,但分配其实是有下层具体文件系统分配出来的,以 ext4 文件系统来说,使用 ext4_alloc_inode 函数分配出 ext4_inode_info 这个大结构体,然后返回的是 inode 的地址而已.
划重点:struct inode 内嵌于具体文件系统的 “inode” 里,vfs 层使用的是 inode,ext4 层使用的是 ext4_inode_info ,不同层次通过地址的强制转化类型来切换结构体.
那么类似,sockfs 也是如此,sockfs 作为文件系统,也有自己特色的 “inode”,这个类型就是 struct socket_alloc ,如下:
这个结构体关联 socket 和 inode 两个角色,是“文件”抽象的核心之一。分配 struct socket 结构体其实是分配了 struct socket_alloc 结构体,然后返回了 socket_alloc->socket 字段的地址而已.
划重点:vfs 层用的时候给 inode 字段的地址,socket 层的时候给 socket 字段的地址。不同抽象层面对于同一个内存块的理解不同,强制转化类型,然后各自使用 。
从文件的角度来看 socket,模块如下:
先铺垫一个小知识点:内核里面有回调唤醒的实现,里面有用到一种 wait queue 的做法,其实很简单的原理.
大白话原理:你要走可以,把联系方式留下,我搞好之后通知你(调用你留下的函数,传入你留下的参数).
拿 socket 来说,struct sock 里面就有个字段 sk_wq ,这是个表头,就是用来挂接等待对象的.
谁会挂?
就以 epoll 池来说,epoll_ctl 注册 socket fd 的时候,就会挂一个 wait 对象到 sk->sk_wq 里。回调参数为 ep_poll_callback ,参数为 epitem .
这样 epoll 给 socket 留下联系方式了( wait 对象 ),socket 有啥事就可以随时通知到 epoll 池了.
能有什么事?
socket 可读可写了呗。sk buffer 里面有数据可以读,或者有空间可以写了呗。对于监听类型的 socket,有新的连接了呗。epoll 监听的不就是这个嘛.
服务端:
客户端:
下面就几个关键函数做个简要实现.
定义原型:
简要跟踪下内部实现:
socket 系统调用对应了 __sys_socket 这个函数。这个函数主要做两件事情:
1. 第一件事:调用 socket_create 函数创建好 socket 相关的结构体,主要是 struct socket ,还有与之关联的 socket sock 结构,再往下就是具体网络协议对应的结构体(旁白:这里实现细节过于复杂,不在文章主干,故略去 10 万字); 。
2. 第二件事:调用 sock_map_fd 函数创建好 struct file 这个结构体,并与第一步创建出的 struct socket 关联起来; 。
涉及的一些函数调用:
先说 socket 函数::
再简要说下内部细节:
sock_create 函数里,会根据协议族查找对应的操作表,以 AF_INET 协议族举例,pf->create 是 inet_create ,主要做两件事:
着重提一点,sock_init_data 函数( net/core/sock.c )主要是初始化 struct sock 结构体的,提两点最关键的:
第一点:接收队列和发送队列在这里初始化; 。
第二点:socket 的唤醒回调在这个地方设置; 。
为什么这里很重要,因为这个跟 socket fd 可读可写的判断逻辑,数据到了之后的唤醒路径息息相关。简述下回调链路(以套接字层为主干,其他的流程简略描述):
再说下结构体:
继续说 struct sock ,这个对象有意思了,这个也是以组合的方式往下兼容的,同一个地址强转类型得到不同层面的结构体。原理就在于:他们是一块连续的内存空间,起始地址相同.
示意图:
小思考:struct socket 和 struct sock 是两个不同的结构体?
是的。这两个是不同的结构体。属于套接字层的两个维度的描述,一个面向上层,一个面向下层.
struct socket 在内核的注释为:
struct socket - general BSD socket 。
struct sock 在内核的注释为:
struct sock_common - minimal network layer representation of sockets 。
struct socket 是内核抽象出的一个通用结构体,主要作用是放置了一些跟 fs 相关的字段,而真正跟网络通信相关的字段结构体是 struct sock 。它们内部有相互的指针,可以获取到对方的地址.
struct socket 这个字段出生的时候其实就和一个 inode 结构体伴生出来的,由 socketfs 的 sock_alloc_inode 函数分配.
struct sock 这个结构体是 socket 套阶字核心的结构(注意,还有个结构是 struct socket,这两个是不同的结构体哦)。这个是对底下具体协议做的一层抽象封装,比如在分配 struct sock 的时候,如果是 tcp 协议,那么 sk->sk_prot 会赋值为 tcp_prot ,udp 协议赋值的是 udp_prot ,之后的一系列协议解析和处理就是调用到对应协议的回调函数.
小思考:socket fd 可以和文件一样用 write(fd, /*xxxx*/ ) 这行的调用,为什么?
write(fd, /*xxxx*/) 进到内核首先是到 vfs 层,也就是调用到 vfs_write ,在这个里面首先获取到 file 这个结构体,然后调用下层注册的回调,比如 file->f_op->write_iter ,file->f_op->write ,所以,关键在 file->f_op 这个字段,对吧?
现在的问题是,这个字段是啥呢?
这个字段在 file 结构体生成的时候,根据你的“文件”类型赋值的,这个在之前文件系统章节提过这个,比如 ext2 的文件,那么就是 ext2_file_operations ,socketfd 是 socket_file_ops.
可以看下 socket_file_ops 的定义:
所以,vfs_write 调用到的将是 sock_write_iter,而这个里面就是调用到 sock_sendmsg ,从而走到网络相关的处理流程.
还记得上面在 socket 初始化的时候 socket->ops 和 sock->sk_prot 两个回调函数操作表的赋值吗( tcp ):
这样从 vfs 进来,转接到具体的协议处理模块去了.
对应内核 __sys_bind 函数,做的事情很简单:
tcp 连接的对应的 bind 函数是 inet_bind,里面做的事情很简单,就是简单的查一下端口有没有被占用,没有被占用的话端口就赋值给 inet_sock->inet_sport 这个字段.
inet_sock 则是由 sk 强转类型得到.
思考个小问题:在上面的图中,bind 这个函数只在服务端用到?
为啥客户端没用这个函数呢?
其实,客户端也是可以用 bind 这个函数,但是没必要.
理解下 bind 函数的作用:给这个 socketfd 绑定地址(IP:Port)用的。客户端不需要是因为:如果没设置,内核在建连的时候会自动选一个临时的端口号作为本次 TCP 连接的地址。一般客户端也不在意端口号,只要能和服务端正常通信就好,所以客户端一般没有 bind 调用.
服务端必须要用这个是因为服务端必须提前明确指定监听的 IP 和 Port (不然谁知道向哪里发起连接呢).
其实 socket( ) 创建出来的套接字并无客户端和服务端之分,是 listen 函数让 socket 有了不一样的属性,成为监听套接字.
listen 系统调用主要做两件事:
inet_listen 做啥了?内核注释:
Move a socket into listening state. 。
简单看下 inet_listen 的实现功能:
inet_csk_listen_start 做啥了?
划重点:套接字的转变就在于此.
inet_accept ( net/ipv4/af_inet.c )注释:
Accept a pending connection. The TCP layer now gives BSD semantics. 。
这个主要是从队列 icsk->icsk_accept_queue 中取请求,如果队列为空,就看 socket 是否设置了非阻塞标识,非阻塞的就直接报错 EAGAIN,否则阻塞线程等待.
所以,监听套接字的可读事件是啥?
icsk_accept_queue 队列非空.
这个队列什么时候被填充的?
这个也是底层网络协议回调往上调用的,tcp 三次握手之后,建立好的连接就在一个队列中 accept_queue ,队列非空则为只读。由 tcp 的协议栈往上调用,对应到 socket 层,还是会调用到 sk->sk_data_ready .
这里还是以 epoll 管理监听套接字来举例。这个跟上面讲的数据来了一样,都是把挂接在 socket 本身上的 wait 对象进行唤醒(调用回调),这样就会到 ep_poll_callback ,ep_poll_callback 就会把监听套接字对应的 ep_item 挂到 epoll 的 ready 队列中,并且唤醒阻塞在 epoll_wait 的线程,从而实现了监听套接字的读事件的触发的流程.
这个没啥讲的,就是由客户端向服务端发起连接的时候调用,一般也和 epoll 配合不起来,略过.
在 深入剖析 epoll 篇 我们就提到过,epoll 池可以管理 socket fd ,用于监听 socket fd 的可读,可写事件。那么问题来了,socket fd 的可读可写事件分别是啥?代表了什么含义?
这个要把服务端的监听类型的 socket fd 和传输数据的 socket fd 分开来说.
监听类型的 fd:
数据类型的 fd:
还有,如果 socket 之上有 pending 的 error 待处理,那么也会触发可读事件.
最后,我们再回忆一下,epoll 池管理的 socket fd 是怎么及时触发唤醒的呢?
换句话说,socket fd 数据就绪之后,怎么能及时的唤醒被阻塞在 epoll_wait 的线程?
还记得套接字 buffer 数据来了的时候的回调吗?
调用的是 sk->sk_data_ready 这个函数指针,这个字段在 socket 初始化的时候被赋值为 sock_def_readable ,这个函数里面会依次调用所有挂接到 socket 的 wait 队列的对象( 表头:sk->sk_wq ),在这个 wait 队列中存在和 epoll 关联的秘密.
回忆下,在 深入剖析 epoll 篇 提到,epoll_ctl 的时候,在把 socket fd 注册进 epoll 池的时候,会把一个 wait 对象挂接到这个 socket 的 sk->sk_wq 中 ,回调函数就是 ep_poll_callback .
这个wait 对象就是数据就绪时候的联系方式,这样把 socket 数据就绪的流程和 epoll 关联上了.
也就是说,sk->sk_data_ready 会调用到 ep_poll_callback ,ep_poll_callback 这个函数处理很简单,做两件事情:
原文链接:https://mp.weixin.qq.com/s/-Cntd83HR1TcSUvmoT5H7A 。
最后此篇关于Linux fd 系列 — socket fd 是什么?的文章就讲到这里了,如果你想了解更多关于Linux fd 系列 — socket fd 是什么?的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于 Stack Overflow 来说是偏离主题的,
Linux 管道可以缓冲多少数据?这是可配置的吗? 如果管道的两端在同一个进程中,但线程不同,这会有什么不同吗? 请注意:这个“同一个进程,两个线程”的问题是理论上的边栏,真正的问题是关于缓冲的。 最
我找到了here [最后一页] 一种有趣的通过 Linux 启动 Linux 的方法。不幸的是,它只是被提及,我在网上找不到任何有用的链接。那么有人听说过一种避免引导加载程序而使用 Linux 的方法
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
我试图了解 ld-linux.so 如何在 Linux 上解析对版本化符号的引用。我有以下文件: 测试.c: void f(); int main() { f(); } a.c 和 b.c:
与 RetroPie 的工作原理类似,我可以使用 Linux 应用程序作为我的桌面环境吗?我实际上并不需要像实际桌面和安装应用程序这样的东西。我只需要一种干净简单的方法来在 RaspberryPi 上
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 这个问题似乎不是关于 a specific programming problem, a softwar
关闭。这个问题是off-topic .它目前不接受答案。 想改进这个问题吗? Update the question所以它是on-topic用于堆栈溢出。 关闭 10 年前。 Improve thi
有什么方法可以覆盖现有的源代码,我应该用 PyQt、PyGTK、Java 等从头开始构建吗? 最佳答案 如果您指的是软件本身而不是它所连接的存储库,那么自定义应用程序的方法就是 fork 项目。据我所
我的情况是:我在一个磁盘上安装了两个 linux。我将第一个安装在/dev/sda1 中,然后在/dev/sda2 中安装第二个然后我运行第一个系统,我写了一个脚本来在第一个系统运行时更新它。
我在 i2c-0 总线上使用地址为 0x3f 的系统监视器设备。该设备在设备树中配置有 pmbus 驱动程序。 问题是,加载 linux 内核时,这个“Sysmon”设备没有供电。因此,当我在总线 0
关闭。这个问题是off-topic .它目前不接受答案。 想改进这个问题吗? Update the question所以它是on-topic用于堆栈溢出。 关闭 11 年前。 Improve thi
我正试图在 linux 模块中分配一大块内存,而 kalloc 做不到。 我知道唯一的方法是使用 alloc_bootmem(unsigned long size) 但我只能从 linux 内核而不是
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 这个问题似乎不是关于 a specific programming problem, a softwar
我有 .sh 文件来运行应用程序。在该文件中,我想动态设置服务器名称,而不是每次都配置。 我尝试了以下方法,它在 CentOS 中运行良好。 nohup /voip/java/jdk1.8.0_71/
我是在 Linux 上开发嵌入式 C++ 程序的新手。我有我的 Debian 操作系统,我在其中开发和编译了我的 C++ 项目(一个简单的控制台进程)。 我想将我的应用程序放到另一个 Debian 操
关闭。这个问题需要多问focused 。目前不接受答案。 想要改进此问题吗?更新问题,使其仅关注一个问题 editing this post . 已关闭 4 年前。 Improve this ques
我使用4.19.78版本的稳定内核,我想找到带有企鹅二进制数据的C数组。系统启动时显示。我需要在哪里搜索该内容? 我在 include/linux/linux_logo.h 文件中只找到了一些 Log
我知道可以使用 gdb 的服务器模式远程调试代码,我知道可以调试针对另一种架构交叉编译的代码,但是是否可以更进一步,从远程调试 Linux 应用程序OS X 使用 gdbserver? 最佳答案 当然
是否有任何可能的方法来运行在另一个 Linux 上编译的二进制文件?我知道当然最简单的是在另一台机器上重建它,但假设我们唯一能得到的是一个二进制文件,那么这可能与否? (我知道这可能并不容易,但我只是
我是一名优秀的程序员,十分优秀!