Linux核心 | socket底層的來龍去脈
Linux核心 | socket底層的來龍去脈
上一篇文章 對Linux sockfs檔案系統的註冊和掛載進行了分析,本文在上文基礎上進一步全面分析socket底層的相關實現。
一、socket與inode
socket在Linux中對應的檔案系統叫Sockfs,每建立一個socket,就在sockfs中建立了一個特殊的檔案,同時建立了sockfs檔案系統中的inode,該inode唯一標識當前socket的通訊。
如下圖所示,左側視窗使用nc工具建立一個TCP連線;右側找到該程序id(3384),通過檢視該程序下的描述符,可以看到"3 ->socket:[86851]",socket表示這是一個socket型別的fd,[86851]表示這個一個inode號,能夠唯一標識當前的這個socket通訊連線,進一步在該inode下檢視"grep -i "86851" /proc/net/tcp”可以看到該TCP連線的所有資訊(連線狀態、IP地址等),只不過是16進位制顯示。
在分析socket與inode之前,先通過ext4檔案系統舉例:
在VFS層,即抽象層,所有的檔案系統都使用struct inode結構體描述indoe,然而分配inode的方式都不同,如ext4檔案系統的分配inode函式是ext4_alloc_inode,如下所示:
static struct inode *ext4_alloc_inode(struct super_block *sb) { struct ext4_inode_info *ei; ei = kmem_cache_alloc(ext4_inode_cachep, GFP_NOFS); if (!ei) return NULL; ei->vfs_inode.i_version = 1; spin_lock_init(&ei->i_raw_lock); INIT_LIST_HEAD(&ei->i_prealloc_list); spin_lock_init(&ei->i_prealloc_lock); ext4_es_init_tree(&ei->i_es_tree); rwlock_init(&ei->i_es_lock); INIT_LIST_HEAD(&ei->i_es_list); ei->i_es_all_nr = 0; ei->i_es_shk_nr = 0; ei->i_es_shrink_lblk = 0; ei->i_reserved_data_blocks = 0; ei->i_da_metadata_calc_len = 0; ei->i_da_metadata_calc_last_lblock = 0; spin_lock_init(&(ei->i_block_reservation_lock)); #ifdef CONFIG_QUOTA ei->i_reserved_quota = 0; memset(&ei->i_dquot, 0, sizeof(ei->i_dquot)); #endif ei->jinode = NULL; INIT_LIST_HEAD(&ei->i_rsv_conversion_list); spin_lock_init(&ei->i_completed_io_lock); ei->i_sync_tid = 0; ei->i_datasync_tid = 0; atomic_set(&ei->i_unwritten, 0); INIT_WORK(&ei->i_rsv_conversion_work, ext4_end_io_rsv_work); return &ei->vfs_inode; }
從函式中可以看出來,函式其實是呼叫kmem_cache_alloc分配了 ext4_inode_info結構體(結構體如下所示),然後進行了一系列的初始化,最後返回的卻是struct inode結構體(如上面程式碼的return &ei->vfs_inode)。如下結構體ext4_inode_info(ei)所示,vfs_inode是其struct inode結構體成員。
struct ext4_inode_info { __le32 i_data[15]; /* unconverted */ __u32 i_dtime; ext4_fsblk_t i_file_acl; ...... struct rw_semaphore i_data_sem; struct rw_semaphore i_mmap_sem; struct inode vfs_inode; struct jbd2_inode *jinode; ...... };
再看一下:ext4_i node、ext4_inode_info、inode之間的關聯,
ext4_inode如下所示,是磁碟上inode的結構
struct ext4_inode { __le16 i_mode; /* File mode */ __le16 i_uid; /* Low 16 bits of Owner Uid */ __le32 i_size_lo; /* Size in bytes */ __le32 i_atime; /* Access time */ __le32 i_ctime; /* Inode Change time */ __le32 i_mtime; /* Modification time */ __le32 i_dtime; /* Deletion Time */ __le16 i_gid; /* Low 16 bits of Group Id */ __le16 i_links_count; /* Links count */ __le32 i_blocks_lo; /* Blocks count */ __le32 i_flags; /* File flags */ ...... }
ext4_inode_info是ext4檔案系統的inode在記憶體中管理結構體:
struct ext4_inode_info { __le32 i_data[15]; /* unconverted */ __u32 i_dtime; ext4_fsblk_t i_file_acl; ...... };
inode是檔案系統抽象層:
struct inode { umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; /* 對inode操作的具體方法 * 不同的檔案系統會註冊不同的函式方法即可 */ const struct inode_operations *i_op; struct super_block *i_sb; struct address_space *i_mapping; unsigned long i_ino; union { const unsigned int i_nlink; unsigned int __i_nlink; }; dev_t i_rdev; /* 檔案大小 */ loff_t i_size; /* 檔案最後訪問時間 */ struct timespec i_atime; /* 檔案最後修改時間 */ struct timespec i_mtime; /* 檔案建立時間 */ struct timespec i_ctime; spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */ unsigned short i_bytes; unsigned int i_blkbits; enum rw_hint i_write_hint; blkcnt_t i_blocks; /* Misc */ unsigned long i_state; struct rw_semaphore i_rwsem; unsigned long dirtied_when; /* jiffies of first dirtying */ unsigned long dirtied_time_when; /* inode通過以下結構被加入到的各種連結串列 */ struct hlist_node i_hash; struct list_head i_io_list; /* backing dev IO list */ struct list_head i_lru; /* inode LRU list */ struct list_head i_sb_list; struct list_head i_wb_list; /* backing dev writeback list */ union { struct hlist_head i_dentry; struct rcu_head i_rcu; }; atomic64_t i_version; atomic_t i_count; atomic_t i_dio_count; atomic_t i_writecount; /* 對檔案操作(如檔案讀寫等)的具體方法 * 實現虛擬檔案系統的核心結構 * 不同的檔案系統只需要註冊不同的函式方法即可 */ const struct file_operations *i_fop; /* former ->i_op->default_file_ops */ struct file_lock_context *i_flctx; struct address_space i_data; struct list_head i_devices; union { struct pipe_inode_info *i_pipe; struct block_device *i_bdev; struct cdev *i_cdev; char *i_link; unsigned i_dir_seq; }; __u32 i_generation; void *i_private; /* fs or device private pointer */ } __randomize_layout;
三者的關係如下圖,struct inode是VFS抽象層的表示,ext4_inode_info是ext4檔案系統inode在記憶體中的表示,struct ext4_inode是檔案系統inode在磁碟中的表示。
VFS採用C語言的方式實現了struct inode和struct ext4_inode_info繼承關係,inode與ext4_inode_info是父類與子類的關係 ,並且Linux核心實現了inode與ext4_inode_info父子類的互相轉換,如下EXT4_I所示:
static inline struct ext4_inode_info *EXT4_I(struct inode *inode) { return container_of(inode, struct ext4_inode_info, vfs_inode); }
以上是以ext4為例進行了分析,下面將開始從socket與inode進行分析:
sockfs是虛擬檔案系統,所以在磁碟上不存在inode的表示,在核心中有struct socket_alloc來表示記憶體中sockfs檔案系統inode的相關結構體:
struct socket_alloc { struct socket socket; struct inode vfs_inode; };
struct socket與struct inode的關係如下圖,正如ext4檔案系統中struct ext4_inode_info與struct inode的關係類似,inode和socket_alloc結構體是父類與子類的關係。
從上面分析ext4檔案系統分配inode時,是通過ext4_alloc_inode函式分配了ext4_inode_info結構體,並初始化結構體成員,函式最後返回的是ext4_inode_info中的struct inode成員。sockfs檔案系統也類似,sockfs檔案系統分配inode時,建立的是socket_alloc結構體,在函式最後返回的是struct inode。
從上篇文章中,分析了sockfs檔案系統註冊與掛載,初始化了超級塊的函式操作集,如下所示alloc_inode是分配inode結構體的回撥函式介面。
static const struct super_operations sockfs_ops = { .alloc_inode = sock_alloc_inode, .destroy_inode = sock_destroy_inode, .statfs = simple_statfs, }
sockfs檔案系統的inode分配函式是sock_alloc_inode,如下所示:
static struct inode *sock_alloc_inode(struct super_block *sb) { struct socket_alloc *ei; struct socket_wq *wq; ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL); if (!ei) return NULL; wq = kmalloc(sizeof(*wq), GFP_KERNEL); if (!wq) { kmem_cache_free(sock_inode_cachep, ei); return NULL; } init_waitqueue_head(&wq->wait); wq->fasync_list = NULL; wq->flags = 0; RCU_INIT_POINTER(ei->socket.wq, wq); ei->socket.state = SS_UNCONNECTED; ei->socket.flags = 0; ei->socket.ops = NULL; ei->socket.sk = NULL; ei->socket.file = NULL; return &ei->vfs_inode; }
sock_alloc_inode函式分配了socket_alloc結構體,也就意味著分配了struct socket和struct inode,並最終返回了socket_alloc結構體成員inode。
故struct socket這個欄位出生的時候其實就和一個struct inode結構體伴生出來的,它們倆共同封裝在struct socket_alloc中,由sockfs的sock_alloc_inode函式分配的,函式返回的是struct inode結構體.和ext4檔案系統型別類似。 sockfs檔案系統也實現了struct inode與struct socket的轉換:
static inline struct socket *SOCKET_I(struct inode *inode) { return &container_of(inode, struct socket_alloc, vfs_inode)->socket; }
二、socket的建立與初始化
首先看一下struct socket在核心中的定義:
struct socket { socket_state state;//socket狀態 short type; //socket型別 unsigned long flags;//socket的標誌位 struct socket_wq __rcu *wq; struct file *file;//與socket關聯的檔案指標 struct sock *sk;//套接字的核心,面向底層網路具體協議 const struct proto_ops *ops;//socket函式操作集 };
在核心中還有struct sock結構體,在struct socket中可以看到那麼它們的關係是什麼?
1、socket面向上層,sock面向下層的具體協議
2、socket是核心抽象出的一個通用結構體,主要是設定了一些跟fs相關的欄位,而真正跟網路通訊相關的欄位結構體是struct sock
3、struct sock是套接字的核心,是對底層具體協議做的一層抽象封裝,比如TCP協議,struct sock結構體中的成員sk_prot會賦值為tcp_prot,UDP協議會賦值為udp_prot。
(關於更多struct sock的分析將在以後的文章中分析)
建立socket的系統呼叫: 在使用者空間建立了一個socket後,返回值是一個檔案描述符。在SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)最後呼叫sock_map_fd進行關聯,其中返回的就是使用者空間獲取的檔案描述符fd,sock就是呼叫sock_create建立成功的socket.
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) { int retval; struct socket *sock; int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock); if (retval < 0) return retval; return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); }
socket的建立將呼叫sock_create函式:
int sock_create(int family, int type, int protocol, struct socket **res) { return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0); }
__sock_create函式呼叫sock_alloc函式分配socket結構和檔案節點:
int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern) { int err; struct socket *sock; const struct net_proto_family *pf; //檢查family的欄位範圍 if (family < 0 || family >= NPROTO) return -EAFNOSUPPORT; if (type < 0 || type >= SOCK_MAX) return -EINVAL; ...... sock = sock_alloc();//分配socket和inode,返回sock if (!sock) { net_warn_ratelimited("socket: no more sockets\n"); return -ENFILE; /* Not exactly a match, but its the closest posix thing */ } sock->type = type; ...... rcu_read_lock(); pf = rcu_dereference(net_families[family]);//獲取協議族family對應的操作表 err = -EAFNOSUPPORT; if (!pf) goto out_release; if (!try_module_get(pf->owner)) goto out_release; /* Now protected by module ref count */ rcu_read_unlock(); err = pf->create(net, sock, protocol, kern);//呼叫family協議族的socket建立函式 if (err < 0) goto out_module_put; if (!try_module_get(sock->ops->owner)) goto out_module_busy; ...... }
socket結構體的建立在sock_alloc()函式中:
struct socket *sock_alloc(void) { struct inode *inode; struct socket *sock; inode = new_inode_pseudo(sock_mnt->mnt_sb); if (!inode) return NULL; sock = SOCKET_I(inode); inode->i_ino = get_next_ino(); inode->i_mode = S_IFSOCK | S_IRWXUGO; inode->i_uid = current_fsuid(); inode->i_gid = current_fsgid(); inode->i_op = &sockfs_inode_ops; this_cpu_add(sockets_in_use, 1); return sock; }
new_inode_pseudo中通過繼續呼叫sockfs檔案系統中的sock_alloc_inode函式完成struct socket_alloc的建立並返回其結構體成員struct inode。
然後呼叫SOCKT_I函式返回對應的struct socket。
在_sock_create中:pf->create(net, sock, protocol, kern);
通過相應的協議族,進一步呼叫不同的socket建立函式。 p f是 struct net_proto_family結構體,如下所示:
struct net_proto_family { int family; int (*create)(struct net *net, struct socket *sock, int protocol, int kern); struct module *owner; };
net_families[]數組裡存放的是各個協議族的資訊,以family欄位作為下標,對應的值為net_pro_family結構體。此處我們針對TCP協議分析,因此我們family欄位是AF_INET,pf->create將呼叫inet_create函式繼續完成底層struct sock等建立和初始化。
inet_create函式完成struct socket、struct inode、struct sock的建立與初始化後, 呼叫sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));完成socket與檔案系統的關聯,負責分配檔案,並與socket進行繫結:
1、呼叫sock_alloc_file,分配一個struct file,並將私有資料指標指向socket結構
2、fd_install 對應檔案描述符和file
static int sock_map_fd(struct socket *sock, int flags) { struct file *newfile; int fd = get_unused_fd_flags(flags);//為socket分配檔案號和檔案結構 if (unlikely(fd < 0)) { sock_release(sock); return fd; } newfile = sock_alloc_file(sock, flags, NULL);//分配file物件 if (likely(!IS_ERR(newfile))) { fd_install(fd, newfile);//使檔案號與檔案結構掛鉤 return fd; } put_unused_fd(fd); return PTR_ERR(newfile); }
get_unused_fd_flags(flags)繼續呼叫alloc_fd完成檔案描述符的分配。
sock_alloc_file(sock, flags, NULL)分配一個struct file結構體
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname) { ...... file = alloc_file(&path, FMODE_READ | FMODE_WRITE, &socket_file_ops);//分配struct file結構體 if (IS_ERR(file)) { /* drop dentry, keep inode for a bit */ ihold(d_inode(path.dentry)); path_put(&path); /* ... and now kill it properly */ sock_release(sock); return file; } sock->file = file; //socket通過其file欄位進行關聯 file->f_flags = O_RDWR | (flags & O_NONBLOCK); file->private_data = sock;//file通過private_data與socket關聯 return file; //返回初始化、關聯後的file結構體 }
其中file = alloc_file(&path, FMODE_READ | FMODE_WRITE,
&socket_file_ops);分配了file結構體並進行初始化:
struct file *alloc_file(const struct path *path, fmode_t mode, const struct file_operations *fop) { struct file *file; file = get_empty_filp(); if (IS_ERR(file)) return file; file->f_path = *path; file->f_inode = path->dentry->d_inode; file->f_mapping = path->dentry->d_inode->i_mapping; file->f_wb_err = filemap_sample_wb_err(file->f_mapping); if ((mode & FMODE_READ) && likely(fop->read || fop->read_iter)) mode |= FMODE_CAN_READ; if ((mode & FMODE_WRITE) && likely(fop->write || fop->write_iter)) mode |= FMODE_CAN_WRITE; file->f_mode = mode; file->f_op = fop; if ((mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ) i_readcount_inc(path->dentry->d_inode); return file; }
其中file->f_op = fop,將socket_file_ops傳遞給檔案操作表
static const struct file_operations socket_file_ops = { .owner = THIS_MODULE, .llseek = no_llseek, .read_iter = sock_read_iter, .write_iter = sock_write_iter, .poll = sock_poll, .unlocked_ioctl = sock_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = compat_sock_ioctl, #endif .mmap = sock_mmap, .release = sock_close, .fasync = sock_fasync, .sendpage = sock_sendpage, .splice_write = generic_splice_sendpage, .splice_read = sock_splice_read, };
以上操作完成了struct socket、struct sock、struct file等的建立、初始化、關聯,並最終返回socket描述符fd
socket描述符fd和我們平時操作檔案的檔案描述符相同,那麼會有一個疑問 ,可以看到struct file_operations socket_file_ops 函式表中並沒有提供write()和read()介面,只是看到read_iter,write_iter等介面,那麼系統是如何處理的呢?
以write()為例:
sys_write()->__vfs_write()
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos) { if (file->f_op->write)//如果檔案函式表結構體提供了write介面函式 return file->f_op->write(file, p, count, pos);//呼叫它的write函式 else if (file->f_op->write_iter) return new_sync_write(file, p, count, pos);//否則呼叫new_sync_write函式 else return -EINVAL; }
從__vfs_write函式中可以看出來,如果socket函式表中沒有提供write介面函式,則呼叫new_sync_write:
static ssize_t new_sync_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos) { ...... ret = call_write_iter(filp, &kiocb, &iter); ...... }
call_write_iter:
static inline ssize_t call_write_iter(struct file *file, struct kiocb *kio,struct iov_iter *iter) { return file->f_op->write_iter(kio, iter);//呼叫socket檔案函式表的aio_write函式 }
從以上__vfs_write()分析,如果檔案函式表結構提供了write介面函式則呼叫write函式,如果檔案函式表結構沒有提供write介面函式(如socket操作函式表中沒有提供write介面),則呼叫write_iter介面,即呼叫socket操作函式表中的sock_write_iter。就這樣通過socket fd進行普通檔案系統那樣通過描述符進行讀寫等。
使用者得到socket fd,可以進行地址繫結、傳送以及接收資料等操作,在Linux核心中有相關的函式完成從socket fd到struct socket、struct file的轉換:
static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed) { struct fd f = fdget(fd);//通過socket fd獲取struct fd結構體,struct fd結構體中有struct file結構 struct socket *sock; *err = -EBADF; if (f.file) { sock = sock_from_file(f.file, err);//通過獲取的struct file結構體獲取相應的struct socket指標 if (likely(sock)) { *fput_needed = f.flags; return sock; } fdput(f); } return NULL; }
fdget()函式從當前程序的files_struct結構中找到網路檔案系統中的file檔案指標,並封裝在struct fd結構體中。sock_from函式通過得到的file結構體得到對應的socket結構指標。sock_from函式如下:
struct socket *sock_from_file(struct file *file, int *err) { if (file->f_op == &socket_file_ops) return file->private_data; /* set in sock_map_fd */ *err = -ENOTSOCK; return NULL; }
至此,socket底層來龍去脈的大體結構大概就分析到這,最為核心的struct sock相關的聯絡以及底層協議的初始化等將在以後的文章進行分析。
END
點個關注 ,一起學技術!

您的點贊和關注是我最大的動力
- 直播回放總結 | eBPF簡介
- armv8-armv9中斷系列詳解-硬體基礎篇
- Linux schedule 之 Cgroup
- 聊聊對 BPF 程式至關重要的 vmlinux.h檔案
- Linux 核心原始碼分析之程序概要及排程時機
- 一文理解 K8s 容器網路虛擬化
- 淺談動態追蹤:從SystemTap到bpfTrace
- 從sk_buff中的線性區與非線性區到bpf_skb_pull_data
- Linux髒頁回寫機制淺析
- Cilium eBPF 搭建與使用
- eBPF技術精彩問答第二期
- eBPF: 深入探究 Map 型別
- 核心trace三板斧-surtrace-cmd
- Linux 核心網路棧分析: 接收資料
- eBPF:流量控制子系統
- eBPF: 從 BPF to BPF Calls 到 Tail Calls
- 群英薈萃,專注小白技術提升--ebpf技術Q&A
- 基於eBPF的CPU利用率精準計算小工具開發
- BPF C 程式設計入門
- eBPF小工具演示直播回放