Linux socket - 源码分析(一)

这篇文章主要分析socket原理和创建流程
参考kernel msm-4.4源码

进程和进程间通信

  进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。不同进程间,内存资源相互独立,无法直接获取和修改,因此不同进程间需要通过特殊的方式进行传递信息。
  进程间通信(IPC,Interprocess communication)是一组编程接口,协调不同的进程,使之能在一个操作系统里同时运行,这些接口给多个进程之间交换信息提供了可能。目前,比较常见的几种跨进程通信方式有:管道(PIPE)、有名管道(FIFO)、消息队列、信号量、共享内存、套接字(socket)等。

  • PIPE:一般指无名管道,只能用于有亲缘关系的父子进程或者兄弟进程间的通信,半双工,数据只能由一端流量另一端
  • FIFO:有名管道与无名管道不同,可以在无关的进程间通信,
  • 消息队列:消息队列由kernel维护的消息链表,一个消息队列由一个标识符确定
  • 信号量:信号量是一个计数器,用于控制多个进程对资源的访问,主要用于实现进程间互斥与同步,不能传递复杂消息
  • 共享内存:由一个进程创建,多个进程可以共享的内存段,需要结合信号量来同步对共享内存的访问
  • socket:网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。这是一种全双工的通信方式,socket的两端既可以读,又可以写。

虚拟文件系统(VFS)

  “Linux下,一切皆是文件“,socket当然也不例外,在系统启动之初,Linux内核也会为socket注册和挂载自己类型的文件系统,为后期socket的创建和使用打下基础。因此,在介绍socket前,有必要简单了解下Linux虚拟文件系统。
  
  虚拟文件系统(Virtual File System,VFS):为了支持挂载不同类型的文件系统,并且为用户进程提供统一的文件操作接口,Linux内核在用户进程和具体的文件系统之间引入了一个抽象层,这个抽象层就称为虚拟文件系统。VSF中四个代表核心概念的结构体:file,dentry,inode,super_block,接下来一一解析:

  • super_block: 对于已挂载的文件系统,VSF在内核中都会生成一个超级块(super_block结构),用于存储文件系统的控制信息,如文件系统类型,大小,所有inode对象等
  • inode:与文件一一对应,在文件系统中是文件的唯一标识,inode包含了文件信息的元数据(大小,权限,类型,时间)和指向文件数据的指针,但不包含文件名。
  • dentry:directory entry(目录项)的缩写,用于建立文件名和相关的inode之间的联系。在上一次扫描时,内核建立和缓存了目录的树状结构,称为dentry cache。通过dentry cache找到file对应的inode,如果没有找到,则需要从磁盘中读取。这样极大加快了查找inode的效率。
  • file:用户进程相关,代表一个打开的文件。每个file结构体都有一个指向dentry的指针,通过dentry可以查找对应的inode。file和inode是多对一的关系,对于同一个文件,系统会为每一次打开都创建一个file结构。

除了上述VSF的4个基本概念,我们也来了解下用户程序操作文件时经常用到的”文件描述符”相关概念:

  • 文件描述符:在Linux中,进程通过文件描述符(file descriptor, fd )来访问文件,fd其实是一个非负整数,是文件描述符表中的编号,通过fd可以在文件描述符表中查找到对应的file结构体。
  • 文件描述符表:每个进程都有自己的文件描述符表,用于记录已经打开过的文件。表中的每一项都有一个指向file结构体的指针,用于查找打开文件的file结构体。

根据上面的描述,基本可以描述用户程序操作一个文件的过程:用户进程通过文件描述符在文件描述表中查找指向对应的file结构体指针,file结构体中包含了dentry指针,内核通过dentry查找到file对应的inode,inode中包含了指向super_block或者保存在disk上的实际文件数据的指针,进而找到实际的文件数据。

注明:对于VFS,这里只是简单描述,具体的原理可以参考如下博文:
https://blog.csdn.net/jnu_simba/article/details/8806654

socket文件系统注册

  socket有自己的文件系统,在内核初始化并调用 do_initcalls 时将socket文件系统注册到内核中。注册过程(net/socket.c):core_initcall(sock_init)

/** core_initcall 宏定义(linux/init.h)*/
#define core_initcall(fn)       __define_initcall(fn, 1)

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn; \

  __define_initcall(fn, id) 指示编译器在编译时声明函数指针__initcall_fn_id并初始化为 fn,同时,将该函数指针变量放置到名为 “.initcall#id.init” 的section数据段中。因此,对于core_initcall(sock_init) ,编译器在编译时会声明__intcallsock_int1并初始化其指向函数指针sock_init,同时存放到.initcall1.init中。这样,内核初始化时通过遍历section拿到sock_init的函数指针地址,可以完成对socket文件系统的注册。
  sock_init:创建,分配socket和inode所需的slab缓存,用于后期使用socket和inode;向内核注册socket文件系统。

/** sock_init (net/socket.c)*/
static int __init sock_init(void)
{
    int err;
    //初始化network sysctl支持
    err = net_sysctl_init();
    if (err)
        goto out;
    //分配skbuff_head_cache和skbuff_fclone_cache slab高速缓存
    skb_init();
    //分配sock_inode_cache slab高速缓存
    init_inodecache();
    //注册socket文件系统
    err = register_filesystem(&sock_fs_type);
    if (err)
        goto out_fs;    
    //挂载socket文件系统
    sock_mnt = kern_mount(&sock_fs_type);
    if (IS_ERR(sock_mnt)) {
        err = PTR_ERR(sock_mnt);
        goto out_mount;
    }
#ifdef CONFIG_NETFILTER
    //初始化netfilter
    err = netfilter_init();
    if (err)
        goto out;
    // ...
#endif

  skbuff_head_cache / skbuff_fclone_cache:与sk_buff相关的两个后备高速缓存(looaside cache),协议栈中使用的所有sk_buff结构都是从这两个高速缓存中分配出来的。两者的不同在于前者指定的单位内存区域大小为

  • 11
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值