本文目标是深入探讨下 sockfd 指向的 socket 到底是啥。首先,socket 当然是文件(Unix 下又有什么不是文件呢),但是我希望能够将 socket 和普通文件联系起来,有一个明确的概念,也就是:文件在 Linux 下到底是什么?Socket 又与其有何区别?

接下来的讨论假定读者已经阅读过 APUE 第三章,对 Unix 文件系统有基本了解。然后我们再来看 Linux 下的文件系统结构。

首先,在 Linux 下,每个进程都有对应的一个 task_struct (include/linux/sched.h)结构体来管理进程。这个结构体的代码长达 617 行,几乎是内核中最复杂的一个结构体了,鉴于其究极复杂程度,这里只摘要出我们所关心的和文件系统相关的代码:

struct task_struct {
	// ...
	/* Filesystem information: */
	struct fs_struct		*fs;

	/* Open file information: */
	struct files_struct		*files;
	// ...
}

我们先看 fs_struct(include/linux/fs_struct.h):

struct fs_struct {
	int users;
	spinlock_t lock;
	seqcount_t seq;
	int umask;
	int in_exec;
	struct path root, pwd;
}
// inlcude/linux/path.h
struct path {
	// ...
	struct vfsmount *mnt;
	struct dentry *dentry;
}

这个结构比较简单,里面保存的是当前文件系统的信息,从变量名可以看出,还保存了 root 和 pwd 信息。其中的 dentry 我们暂且不管。

然后再看 files_struct(include/linux/fdtable.h),同样的,我们将细节代码省去:

struct files_struct {
	// ...
	struct fdtable __rcu *fdt;
	// ...
}

struct fdtable {
	unsigned int max_fds;
	struct file __rcu **fd;      /* current fd array */
	unsigned long *close_on_exec;
	unsigned long *open_fds;
	unsigned long *full_fds_bits;
	struct rcu_head rcu;
};

可以看到,files_struct 最主要的内容就是一个指针指向一个 file 结构的数组,数组的大小由 max_fds 标记。

Note: 注意,我们通过 open 或者 socket 拿到的 fd 其实就是这个数组的 index,到现在为止,我们的结构及信息都是进程内部私有的,struct file __rcu **fd; 指向的 file 结构则是所谓的全局打开文件表了。

file(include/linux/fs.h)结构的关键代码如下:

struct file {
	// ...
	struct path		f_path;
	struct inode		*f_inode;	/* cached value */
	const struct file_operations	*f_op;

	unsigned int 		f_flags;
	fmode_t			f_mode;
	struct mutex		f_pos_lock;
	loff_t				f_pos;
}

其中,打开文件的状态标志及偏移量就记录在 file 结构内。我们暂时忽略 file_operations,那么,其中最关键的结构就是 f_path 中包含的 dentry 及大名鼎鼎的 inode 了。

我们先来看 dentry(include/linux/dcache.h):

struct dentry {
	struct dentry *d_parent;	/* parent directory */
	struct qstr d_name;
	struct inode *d_inode;		/* Where the name belongs to - NULL is
					 				* negative */

	const struct dentry_operations *d_op;
	
	struct super_block *d_sb;	/* The root of the dentry tree */
	struct list_head d_child;	/* child of parent list */
	struct list_head d_subdirs;	/* our children */
}

dentry 代表的就是操作系统描述文件系统时,给出的目录项的概念。我们可以想象一个文件拥有很多信息,包括文件长度、磁盘位置、创建时间、更新时间等,这些信息 Linux 由 inode 结构来管理,同时,由于符号链接的存在,每个物理文件可以由多个逻辑文件名,所以和文件名相关信息交由 dentry 结构管理。同样的,我们暂时忽略 dentry_operations 这个结构。

其中 d_inode 指针指向对应的 inode 结构。inode(include/linux/fs.h)结构的关键代码如下:

struct inode {
	// ...
	const struct inode_operations	*i_op;
	
	struct super_block	*i_sb;
	loff_t				i_size;	// 文件大小,字节数
	blkcnt_t            i_blocks; // 文件大小,块数
	struct timespec64	i_atime;
	struct timespec64	i_mtime;
	struct timespec64	i_ctime;
}

如上文所述,关于文件的大部分信息都保存在 inode 结构中。到目前为止,我们忽略了 file_operationsdentry_operationsinode_operations,我们现在来看下这些结构是干嘛的:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	// ...
};

struct dentry_operations {
	int (*d_delete)(const struct dentry *);
	int (*d_init)(struct dentry *);
	char *(*d_dname)(struct dentry *, char *, int);
	// ...
};

struct inode_operations {
	int (*create) (struct inode *,struct dentry *, umode_t, bool);
	int (*link) (struct dentry *,struct inode *,struct dentry *);
	int (*unlink) (struct inode *,struct dentry *);
	int (*symlink) (struct inode *,struct dentry *,const char *);
	int (*mkdir) (struct inode *,struct dentry *,umode_t);
	int (*rmdir) (struct inode *,struct dentry *);
	// ...
};

这样子一下子就清晰了,operations 这种结构就是保存这各类适用于该结构的函数指针,具体函数实现再交由如 ext2 这样的具体文件系统来实现。

到现在为止,我们终于对 Linux 文件系统有一个较为清晰的认识了:


讲了这么多,我们总算可以继续探讨最开始的问题:socket 到底是啥?

当然,socket 是一个文件,不同于一开始,我们现在可以从进程到 inode 都有一个非常正确的认知,不过,我们还需要继续思考,socket 又是如何与 inode 联系起来的?

答案要回到我们最初的调用 int socket(...) 中,最终这个函数会调用 sock_alloc_inode(...) 来创建 inode,sock_alloc_inode(net/socket.c)中我们关心的代码如下:

static struct inode *sock_alloc_inode(struct super_block *sb)
{ // Linux 是大括号换行党 ==!
	struct socket_alloc *ei;
	// ...
	ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
	// ...
	return &ei->vfs_inode;
};

看代码发现,虽然这个函数是要创建 inode,实际上函数里却创建了 socket_alloc 结构,socket_alloc(include/net/sock.h):

struct socket_alloc {
	struct socket socket;
	struct inode vfs_inode;
};

继续看看 socket(include/linux/net.h)结构:

struct socket {
	socket_state		state;

	short			type;

	unsigned long		flags;

	struct socket_wq	*wq;

	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;
};

现在看到 proto_ops 想必不会疑惑了,来看看在 socket 这个结构上,定义了哪些函数指针:

struct proto_ops {
	int		family;

	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags, bool kern);
	__poll_t	(*poll)	     (struct file *file, struct socket *sock,
				      struct poll_table_struct *wait);
	int		(*listen)    (struct socket *sock, int len);
	int		(*shutdown)  (struct socket *sock, int flags);
	// ....
};

嗯。。。至少这些函数看起来就非常熟悉了。

另外,socket 结构中,另一个重要结构是 sock,下面把 sock(include/net/sock.h)感兴趣的字段列出来:

// sock 结构非常复杂,贯穿各层
struct sock {
	// 待读包队列
	struct sk_buff_head	sk_receive_queue;
	// receive buffer size
	int			sk_rcvbuf;
	// send buffer size
	int			sk_sndbuf;
	// 待写包队列
	struct sk_buff_head	sk_write_queue;
	// ...
};

其中,sk_buff 是网络数据报在内核中的表现形式。不过我们不再往下探究了。

剩下的最后一个问题就是,内核又是如何通过 inode 获得对应的 socket 的呢?

答案在 SOCKET_I(include/net/sock.h)这个函数里:

static inline struct socket *SOCKET_I(struct inode *inode)
{
	return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
}

终于,socket 与 Linux 文件系统的关系,已经了然于胸。