精华内容
下载资源
问答
  • 内核的socket编程

    千次阅读 2013-06-17 15:32:58
    记不清从哪个kernel...不过好在kernel提供了一组内核的socket API。在net/socket.c文件,可以看到这么几个导出符号: EXPORT_SYMBOL(kernel_sendmsg); EXPORT_SYMBOL(kernel_recvmsg); EXPORT_SYMBOL(sock_crea

    记不清从哪个kernel版本开始,内核态就不能直接使用系统调用了。当然,socket的系统调用也不能用了。不过好在kernel提供了一组内核态的socket API。在net/socket.c文件中,可以看到这么几个导出符号:

    EXPORT_SYMBOL(kernel_sendmsg);
    EXPORT_SYMBOL(kernel_recvmsg);
    EXPORT_SYMBOL(sock_create_kern);
    EXPORT_SYMBOL(sock_release);
    EXPORT_SYMBOL(kernel_bind);
    EXPORT_SYMBOL(kernel_listen);
    EXPORT_SYMBOL(kernel_accept);
    EXPORT_SYMBOL(kernel_connect);
    EXPORT_SYMBOL(kernel_getsockname);
    EXPORT_SYMBOL(kernel_getpeername);
    EXPORT_SYMBOL(kernel_getsockopt);
    EXPORT_SYMBOL(kernel_setsockopt);
    EXPORT_SYMBOL(kernel_sendpage);
    EXPORT_SYMBOL(kernel_sock_ioctl);
    EXPORT_SYMBOL(kernel_sock_shutdown);
    

    基本上,在用户态的socket的API,在内核态都有对应的API。


    下面是一个项目中的代码,socket操作我加了注视。不用去关心代码功能,只要看一下socket部分的操作即可,非常简单。

    #include <linux/socket.h>
    #include <linux/net.h>
    #include <linux/in.h>
    
    struct fsg_common{
    ....
            struct socket *encrypt_sock; 
            struct msghdr encrypt_msg;
            struct sockaddr_in encrypt_servaddr;
            struct data_pkt *encrypt_pkt;
    }
    
    static int encrypt_socket_init(struct fsg_common *common)
    {
    	struct socket *sock;
    	int ret;
    	char *dst_addr = "111.111.111.111";
    	/* init servaddr */
    	memset(&common->encrypt_servaddr, 0, sizeof(common->encrypt_servaddr));
    	common->encrypt_servaddr.sin_family = AF_INET;
    	common->encrypt_servaddr.sin_port = htons(9999);
    	
    	//kernel态的IP地址转换函数,有两个函数in4_pton和in6_pton,分别用于IPv4和IPv6
    	in4_pton(dst_addr, strlen(dst_addr), (u8*)&common->encrypt_servaddr.sin_addr, '\0', NULL);
    	
    	//内核态udp通信地址使用struct msghdr封装
    	common->encrypt_msg.msg_name = &common->encrypt_servaddr;
    	common->encrypt_msg.msg_namelen = sizeof(common->encrypt_servaddr);
    
    	//创建套接字,类似socket()函数,返回的sock指针就是后面数据传递用的套接字了
    	ret = sock_create_kern(AF_INET, SOCK_DGRAM, IPPROTO_UDP, &sock);
    	if (ret < 0 || NULL == sock) {
    		ERROR(common, "init encrypt board socket fail\n");
    		return ret;
    	}
    	common->encrypt_sock = sock;
    
    	common->encrypt_pkt = kmalloc(sizeof(struct data_pkt), GFP_KERNEL);
    	if (NULL == common->encrypt_pkt)
    		return -1;
    
    	return 0;
    }
    
    static void encrypt_socket_close(struct fsg_common *common)
    {
    	//关闭套接字,类似用户态的close()函数
    	sock_release(common->encrypt_sock);
    	common->encrypt_sock = NULL;
    }
    
    static int encrypt_data(struct fsg_common *common, u8* data, unsigned int size, loff_t offset)
    {
    	struct socket *sock = common->encrypt_sock; 
    	struct data_pkt *pkt = common->encrypt_pkt;
    	struct sockaddr *pservaddr = (struct sockaddr *)&common->encrypt_servaddr;
    	int servlen = sizeof(common->encrypt_servaddr);
    
    	if (unlikely(NULL == sock || NULL == pkt || NULL == pservaddr))
    		return -1;
    
    	memset(pkt, 0, sizeof(struct data_pkt));
    	unsigned int copied_size = 0;
    	struct kvec vec;
    	struct msghdr msg;
    	memset(&msg, 0, sizeof(msg));
    
    	while (copied_size < size) {
    		pkt->res=0xffffffff;
    		pkt->file_offset = offset; 
    		memcpy(pkt->data, data + copied_size, SECTOR_SIZE);
    
    		//配置udp发送数据地址及长度
    		vec.iov_base = (void *)pkt;
    		vec.iov_len = sizeof(struct data_pkt);
    		ret = kernel_sendmsg(sock, &common->encrypt_msg, &vec, 1, sizeof(struct data_pkt));
    		printk("send data(return :%d):\n", ret);
    
    		//接收udp报文
    		ret = kernel_recvmsg(sock, &msg, &vec, 1, sizeof(struct data_pkt), 0);
    		printk("recv data(return :%d):\n", ret);
    		memcpy(data+copied_size, pkt->data, 512);
    		copied_size += 512;
    	}   
    
    	return 0;
    }
    可以看出来,kernel态的udp通信除了具体API及参数不同,过程和用户态是完全一样的。





    展开全文
  • 本文将使用 bcc工具抓取内核网络中的数据,包括抓取 backlog 信息、port 和 IP 信息、网络命名空间信息等。 eBPF 可以将任何内核函数调用转换成可带任何数据的用户空间事件。每当有程序监听 TCP socket,就得到一个...

    bcc 是基于 LLVM 的工具集,用 Python 封装了底层机器相关的细节,bcc工具使得 eBPF 的使用更加方便,使用时内核探测代码用 C 写, 数据处理用 Python 。本文将使用 bcc工具抓取内核网络中的数据,包括抓取 backlog 信息、port 和 IP 信息、网络命名空间信息等。

    eBPF 可以将任何内核函数调用转换成可带任何数据的用户空间事件。每当有程序监听 TCP socket,就得到一个事件通知。当在 AF_INETSOCK_STREAM 类型的 socket 上调用系统调用 listen() 时,底层负责处理的内核函数就是 inet_listen()。我们可以用 kprobe 在它的入口做 hook,从打印一个 Hello, World开始。

    1. bcc程序代码:

    from bcc import BPF
    
    # Hello BPF Program
    bpf_text = """
    #include <net/inet_sock.h>
    #include <bcc/proto.h>
    
    // 1. Attach kprobe to "inet_listen"
    int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)
    {
        bpf_trace_printk("Hello World!\\n");
        return 0;
    };
    """
    
    # 2. Build and Inject program
    b = BPF(text=bpf_text)
    
    # 3. Print debug output
    while True:
        print b.trace_readline()
    
    

    这个程序做了 3 件事情:

    • 依据命名规则,将探测点 attach 到 inet_listen 函数;
    • 使用 LLVM eBPF 编译,将生成的字节码用 bpf() 系统调用注入(inject)内核,并自动根据命名规则 attach 到 probe 点;
    • 从内核管道读取原始格式的输出,bpf_trace_printk() 可以将 tracing 信息打印到 /sys/kernel/debug/tracing/trace_pipe 下面的一个特殊管道。

    2. 运行结果

    运行此bcc程序,在另一个终端输入nc -l 0 4242,可以看到此bcc程序运行结果如下:

    nc 是个单连接的小工具。

    在这里插入图片描述

    3. 获取 backlog 信息

    “backlog” 是 TCP socket 允许建立的最大连接的数量(等待被 accept())。

    将程序中的 bpf_trace_printk 修改为如下:

    bpf_trace_printk("Listening with with up to %d pending connections!\\n", backlog);
    

    重新运行程序,在另一个终端输入nc -l 0 4242,因为 nc 是一个单链接的小工具,所以 backlog 是 1,运行结果如下:
    在这里插入图片描述

    4. 获取 port 和 IP 信息

    阅读内核 inet_listen 源码发现,我们需要从 socket 对象中拿到 inet_sock 字段 。从内核直接拷贝这两行代码,放到我们 tracing 程序的开始处:

    // cast types. Intermediate cast not needed, kept for readability
    struct sock *sk = sock->sk;
    struct inet_sock *inet = inet_sk(sk);
    

    port 可以从 inet->inet_sport 中获得,注意是网络序(大端)。IP 从 inet->inet_rcv_saddr 读取。现在将程序中的 bpf_trace_printk 修改为如下:

    bpf_trace_printk("Listening on %x %d with %d pending connections\\n", inet->inet_rcv_saddr, inet->inet_sport, backlog);
    

    重新运行程序,在另一个终端输入nc -l 0 4242,运行结果如下:
    在这里插入图片描述

    5. 获取网络空间命名信息

    在用户空间,可以在/proc/PID/ns/net查看网络命名空间,格式类似于 net:[4026531957]。中括号中的数字是网络命名空间的 inode。这意味着,想获取命名空间,我们可以直接去读 proc 。但是,这种方式只适用于运行时间比较短的进程,而且还存在竞争。下面我们使用 eBPF 从 kernel 直接读取 inode 值,添加获取网络命名空间功能后的完整bcc程序如下:

    from bcc import BPF
    
    # Hello BPF Program
    bpf_text = """
    #include <net/inet_sock.h>
    #include <bcc/proto.h>
    #include <net/sock.h>
    // 1. Attach kprobe to "inet_listen"
    int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog)
    {
        // cast types. Intermediate cast not needed, kept for readability
        struct sock *sk = sock->sk;
        struct inet_sock *inet = (struct inet_sock *)sk;
        // Create an populate the variable
    	u32 netns = 0;
    
    	// Read the netns inode number, like /proc does
    	netns = sk->__sk_common.skc_net.net->ns.inum;
    
        bpf_trace_printk("Listening on %x %d with %d pending connections in container %d \\n", inet->inet_rcv_saddr, inet->inet_sport, backlog, netns);
        return 0;
    };
    """
    
    # 2. Build and Inject program
    b = BPF(text=bpf_text)
    
    # 3. Print debug output
    while True:
        print b.trace_readline()
    
    
    

    运行时出现了这样的错误:

    error: <unknown>:0:0: in function kprobe__inet_listen i32 (%struct.pt_regs*): too many args to 0x55a83e8f8320: i64 = Constant<6>
    

    Clang 想告诉我们:bpf_trace_printk 带的参数太多了,这是 BPF 的限制。解决这个问题的办法就是使用 perf,它支持传递任意大小的结构体到用户空间。(注意需要 Linux 4.4 以上内核)

    今天先分享到这里,下篇文章继续分享具体解决办法以及在eBPF内核探测中如何将任意系统调用转换成事件。

    展开全文
  • 首先来看整个与socket相关操作提供了一个统一接口sys_socketcall.  下面就是它代码片段: asmlinkage long sys_socketcall(int call, unsigned long __user *args) { unsigned long a[6]; unsigned long...

    首先来看整个与socket相关的操作提供了一个统一的接口sys_socketcall. 

    下面就是它的代码片段:

    asmlinkage long sys_socketcall(int call, unsigned long __user *args)  
    {  
        unsigned long a[6];  
        unsigned long a0, a1;  
        int err;  
    
      
        a0 = a[0];  
        a1 = a[1];  
      
        switch (call) {  
        case SYS_SOCKET:  
            err = sys_socket(a0, a1, a[2]);  
            break;  
        case SYS_BIND:  
            err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);  
            break;  
        case SYS_CONNECT:  
            err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);  
            break;  
        case SYS_LISTEN:  
            err = sys_listen(a0, a1);  
            break;  
        case SYS_ACCEPT:  
            err =  
                do_accept(a0, (struct sockaddr __user *)a1,  
                      (int __user *)a[2], 0);  
            break;  
        case SYS_GETSOCKNAME:  
            err =  
                sys_getsockname(a0, (struct sockaddr __user *)a1,  
                        (int __user *)a[2]);  
            break;  
     
        return err;  
    }  
    
    可以看到代码比较简单,就是通过传递进来的call类型,来调用相应的socket相关的函数. 

    这里你可能注意到了,那就是一般文件句柄相关的操作,比如write,read,aio,poll这些并没有看到(也就是file_operations).这是因为socket上面其实还有一层vfs层,内核把socket当做一个文件系统来处理,并实现了相应的vfs方法.因此下面我们先来了解下vfs.然后会描述下进程如何通过vfs存取句柄. 

    vfs其实就相当于对下层的文件系统和上层应用之间的粘合层,它定义了文件系统需要实现的相关的操作,然后下层的文件系统只需要实现这些方法就可以了,也就是说在内核其他部分和上层应用看来,所有的文件系统没有任何区别. 

    下面的这张图就是从用户空间调用write的大体流程: 



    vfs中有4种主要的数据结构: 

    1 超级块对象,代表一个已安装的文件系统.super_block 

    2 索引节点对象,代表一个文件.inode 

    3 目录项对象,代表一个目录项.dentry 

    4 文件对象,表示一个被进程打开的文件.file 

    其中每种对象都包含一个操作对象.依次为super_operations,inode_operations,dentry_operations以及file_operations.各自操作不同的层次.然后我们的文件系统只需要实现这些方法,然后注册到内核就可以了. 

    接下来我们来看和vfs相应的结构: 

    第一个就是file_system_type结构,这个结构表示了一个文件系统: 
    struct file_system_type {  
        const char *name;  
        int fs_flags;  
    ///最关键的函数,得到文件系统的超级块.  
        int (*get_sb) (struct file_system_type *, int,  
                   const char *, void *, struct vfsmount *);  
        void (*kill_sb) (struct super_block *);    
    };  

    然后是vfsmount结构,它表示了一个安装点,换句话说也就是一个文件系统实例. 

    第三个是files_struct结构,它主要是为每个进程来维护它所打开的句柄.这里只需要注意一个就是fd_array和fstable中的fd的区别.当进程数比较少也就是小于NR_OPEN_DEFAULT(32)时,句柄就会存放在fd_array中,而当句柄数超过32则就会重新分配数组,然后将fd指针指向它(然后我们通过fd就可以取得相应的file结构). 

    而且files_struct是每个进程只有一个的.

    struct files_struct {  
      /* 
       * read mostly part 
       */  
        atomic_t count;  
        struct fdtable *fdt;  
        struct fdtable fdtab;  
      /* 
       * written part on a separate cache line in SMP 
       */  
        spinlock_t file_lock ____cacheline_aligned_in_smp;  
        int next_fd;  
        struct embedded_fd_set close_on_exec_init;  
        struct embedded_fd_set open_fds_init;  
    ///所打开的所有文件  
        struct file * fd_array[NR_OPEN_DEFAULT];  
    };  
      
      
    struct fdtable {  
        unsigned int max_fds;  
        struct file ** fd;      /* current fd array */  
        fd_set *close_on_exec;  
        fd_set *open_fds;  
        struct rcu_head rcu;  
        struct fdtable *next;  
    };  
    还有两个一个是fs_struct,一个是namespace也都是进程相关的.这里就不一一介绍了. 

    我这里vfs介绍只是个大概,需要详细了解的,可以去看ulk的vfs相关章节和linux内核设计与实现的相关章节. 

    因此下面的图表示了进程和socket的关系: 




    上面的这张图有些老了,新的内核中的inode节点中已经没有u这个联合体了,对应的是会有一个包含socket和inode的一个结构体,然后我们通过inode,而inode中专门有个i_mode域来判断相应的inode类型,比如socket就是 S_IFSOCK.就可以直接计算出相应的socket的地址,然后就可以存取socket了.后面我们会介绍. 

    内核中标售socket有两个数据结构,一个是socket,另一个是sock,其中socket是一个general BSD socket, 它也就是应用程序和4层协议之间的一个接口,屏蔽掉了相关的4层协议部分.而在内核中,socket所需要使用的相关的4层协议的信息全部是保存在sock结构当中的,而socket和sock这两个结构都有保存对方的指针,因此可以很容易的存取对方.

    还有一个就是ops域,这个域保存了所有的相关的4层协议的操作函数.. 

    而在sock中有一个sk_common保存了一个skc_prot域,这个域保存的是相应的协议簇的操作函数的集合. 


    后面介绍到socket创建的时候,我们会分析proto_ops和proto的区别.其实proto相当于对proto_ops的一层封装,最终会在proto中调用proto_ops. 

    /** 
     *  struct socket - general BSD socket 
     *  @state: socket state (%SS_CONNECTED, etc) 
     *  @type: socket type (%SOCK_STREAM, etc) 
     *  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc) 
     *  @ops: protocol specific socket operations 
     *  @fasync_list: Asynchronous wake up list 
     *  @file: File back pointer for gc 
     *  @sk: internal networking protocol agnostic socket representation 
     *  @wait: wait queue for several uses 
     */  
    struct socket {  
        socket_state        state;  
        short           type;  
        unsigned long       flags;  
        const struct proto_ops  *ops;  
        struct fasync_struct    *fasync_list;  
        struct file     *file;  
        struct sock     *sk;  
        wait_queue_head_t   wait;  
    };  
      
    struct sock_common {  
        unsigned short      skc_family;  
        volatile unsigned char  skc_state;  
        unsigned char       skc_reuse;  
        int         skc_bound_dev_if;  
        struct hlist_node   skc_node;  
        struct hlist_node   skc_bind_node;  
        atomic_t        skc_refcnt;  
        unsigned int        skc_hash;  
        struct proto        *skc_prot;  
    #ifdef CONFIG_NET_NS  
        struct net      *skc_net;  
    #endif  
    };  
      
    struct proto_ops {  
        int     family;  
        struct module   *owner;  
        int     (*release)   (struct socket *sock);  
        int     (*bind)      (struct socket *sock,  
                          struct sockaddr *myaddr,  
                          int sockaddr_len);  
        int     (*connect)   (struct socket *sock,  
                          struct sockaddr *vaddr,  
                          int sockaddr_len, int flags);  
     
    };  
    然后我们来看sock_init的实现,在这个函数中,将socket注册为一个伪文件系统,并安装相应的mount点: 

    ///相应的mount对象  
    static struct vfsmount *sock_mnt __read_mostly;  
    ///文件系统对象.  
    static struct file_system_type sock_fs_type = {  
        .name =     "sockfs",  
        .get_sb =   sockfs_get_sb,  
        .kill_sb =  kill_anon_super,  
    };  
      
    static int __init sock_init(void)  
    {  
        /* 
         *      Initialize sock SLAB cache. 
         */  
      
        sk_init();  
      
        /* 
         *      Initialize skbuff SLAB cache 
         */  
        skb_init();  
      
    ///初始化一个inodecache.  
        init_inodecache();  
    ///注册文件系统到内核.  
        register_filesystem(&sock_fs_type);  
    ///安装mount点.  
        sock_mnt = kern_mount(&sock_fs_type);  
      
    #ifdef CONFIG_NETFILTER  
        netfilter_init();  
    #endif  
        return 0;  
    } 
    我们知道每次创建一个socket,都是要依赖于当前的protocol family类型的(后面会分析sys_socket的源码的时候会看到).而在内核中,每种类型的protocol family都会有一个相对应的net_proto_family结构,然后将这个结构注册到内核的net_families数组中,这样我们创建socket的时候,就可以调用这个数组来创建socket. 

    我们先来看sock_register的源码,也就是如何将一个net_proto_family注册到相应的数组: 

    static const struct net_proto_family *net_families[NPROTO] __read_mostly;  
      
    int sock_register(const struct net_proto_family *ops)  
    {  
        int err;  
      
        if (ops->family >= NPROTO) {  
            printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family,  
                   NPROTO);  
            return -ENOBUFS;  
        }  
      
        spin_lock(&net_family_lock);  
    ///代码非常简单,就是根据类型,然后放到相应的位置.  
        if (net_families[ops->family])  
            err = -EEXIST;  
        else {  
            net_families[ops->family] = ops;  
            err = 0;  
        }  
        spin_unlock(&net_family_lock);  
      
        printk(KERN_INFO "NET: Registered protocol family %d\n", ops->family);  
        return err;  
    }  
    我们知道每个协议簇和相应的套接口都对应有好多种组合,因此在协议簇的实现中保存了一个相应的结构来保存这些组合,然后后面就首先通过family然后确定到某个结构,再根据套接口的类型来得到这个结构,并赋值给sock. 

    这里要注意我们只分析af_inet的实现,其他的协议簇都差不多: 

    我们来看这个的实现: 

    ///可以看到这是一个数组,每个元素都是一个链表,也就是每种类型的socket就是一个链表.而这个链表所包含的是不同4层协议的inetsw.可是在inet中,现在每种类型的socket只对应一个4层协议.这里只是为了以后扩展.  
    static struct list_head inetsw[SOCK_MAX];  
      
    ///相应的socket的对应的信息的结构.  
    struct inet_protosw {  
        struct list_head list;  
      
    ///需要这两个key才能定位一个inet_protosw.  
        unsigned short   type;     /* This is the 2nd argument to socket(2). */  
        unsigned short   protocol; /* This is the L4 protocol number.  */  
      
    ///相应的基于ipv4的4层协议的操作集合.  
        struct proto     *prot;  
    ///相应的协议簇的操作信息.  
        const struct proto_ops *ops;  
        
        int              capability; /* Which (if any) capability do 
                          * we need to use this socket 
                          * interface? 
                                          */  
        char             no_check;   /* checksum on rcv/xmit/none? */  
        unsigned char    flags;      /* See INET_PROTOSW_* below.  */  
    };  
      
    void inet_register_protosw(struct inet_protosw *p)  
    {  
        struct list_head *lh;  
        struct inet_protosw *answer;  
        int protocol = p->protocol;  
        struct list_head *last_perm;  
     
        answer = NULL;  
        last_perm = &inetsw[p->type];  
    ///这个操作也很简单,就是将inet_protosw根据套接口类型插入到全局链表数组.  
        list_for_each(lh, &inetsw[p->type]) {  
            answer = list_entry(lh, struct inet_protosw, list);  
      
            /* Check only the non-wild match. */  
            if (INET_PROTOSW_PERMANENT & answer->flags) {  
                if (protocol == answer->protocol)  
                    break;  
                last_perm = lh;  
            }  
      
            answer = NULL;  
        }  
        if (answer)  
            goto out_permanent;  
    ///插入链表.  
        list_add_rcu(&p->list, last_perm);  


    接下来来分析inet_init的源码. 

    ///表示了所有的可能的当前协议簇和套接口类型的组合.  
    static struct inet_protosw inetsw_array[] =  
    {  
        {  
            .type =       SOCK_STREAM,  
            .protocol =   IPPROTO_TCP,  
            .prot =       &tcp_prot,  
            .ops =        &inet_stream_ops,  
            .capability = -1,  
            .no_check =   0,  
            .flags =      INET_PROTOSW_PERMANENT |  
                      INET_PROTOSW_ICSK,  
        },  
      
        {  
            .type =       SOCK_DGRAM,  
            .protocol =   IPPROTO_UDP,  
            .prot =       &udp_prot,  
            .ops =        &inet_dgram_ops,  
            .capability = -1,  
            .no_check =   UDP_CSUM_DEFAULT,  
            .flags =      INET_PROTOSW_PERMANENT,  
           },  
      
      
           {  
               .type =       SOCK_RAW,  
               .protocol =   IPPROTO_IP,    /* wild card */  
               .prot =       &raw_prot,  
               .ops =        &inet_sockraw_ops,  
               .capability = CAP_NET_RAW,  
               .no_check =   UDP_CSUM_DEFAULT,  
               .flags =      INET_PROTOSW_REUSE,  
           }  
    };  
      
    ///协议簇的创建函数.  
    static struct net_proto_family inet_family_ops = {  
        .family = PF_INET,  
        .create = inet_create,  
        .owner  = THIS_MODULE,  
    };  
      
    static int __init inet_init(void)  
    {   
      
    ///注册相应的proto到全局链表中.  
        rc = proto_register(&tcp_prot, 1);  
        if (rc)  
            goto out;  
      
        rc = proto_register(&udp_prot, 1);  
        if (rc)  
            goto out_unregister_tcp_proto;  
      
        rc = proto_register(&raw_prot, 1);  
        if (rc)  
            goto out_unregister_udp_proto;  
      
    ///注册协议簇的操作函数(后面socket创建的时候会用到).  
        (void)sock_register(&inet_family_ops);  
    
        /* Register the socket-side information for inet_create. */  
        for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)  
            INIT_LIST_HEAD(r);  
      
    ///将inetsw_array插入到相应的数组链表.  
        for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)  
            inet_register_protosw(q);  
      
    } 
    接下来我们来通过分析创建socket的函数sys_socket,来更加好的理解socket的实现.

    asmlinkage long sys_socket(int family, int type, int protocol)  
    {   
    ///主要是两个函数,一个是创建socket  
        retval = sock_create(family, type, protocol, &sock);  
        if (retval < 0)  
            goto out;  
    ///这个是相应的文件系统的操作.  
        retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));  
    }  
    sock_create的具体流程我们就不分析了,我们只需要知道最终他会通过传递进来的family的值,来取得相应的family中注册的creat函数.然后会调用这个函数来完成socket的创建.而在上面的代码分析中,我们知道在af_inet中,注册的create函数是inet_create函数,因此我们来看这个函数的实现: 
    static int inet_create(struct net *net, struct socket *sock, int protocol)  
    {  
        struct sock *sk;  
        struct inet_protosw *answer;  
        struct inet_sock *inet;  
        struct proto *answer_prot;  
        unsigned char answer_flags;  
        char answer_no_check;  
        int try_loading_module = 0;  
        int err;  
    ///首先给socket状态赋值.  
      
        sock->state = SS_UNCONNECTED;  
      
        /* Look for the requested type/protocol pair. */  
    lookup_protocol:  
        err = -ESOCKTNOSUPPORT;  
        rcu_read_lock();  
    ///通过type和protocl的值,来查找到相应的inet_protosw结构.  
        list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {  
      
            err = 0;  
            /* Check the non-wild match. */  
            if (protocol == answer->protocol) {  
                if (protocol != IPPROTO_IP)  
                    break;  
            } else {  
                /* Check for the two wild cases. */  
                if (IPPROTO_IP == protocol) {  
                    protocol = answer->protocol;  
                    break;  
                }  
                if (IPPROTO_IP == answer->protocol)  
                    break;  
            }  
            err = -EPROTONOSUPPORT;  
        }  
      
    ///开始给socket赋值.这里我们可以看到最终socket的ops域所得到的值就是相应的协议簇的操作集合(比如inet_stream_ops这些)..  
        sock->ops = answer->ops;  
        answer_prot = answer->prot;  
        answer_no_check = answer->no_check;  
        answer_flags = answer->flags;  
        rcu_read_unlock();  
      
        WARN_ON(answer_prot->slab == NULL);  
      
        err = -ENOBUFS;  
    ///alloc一个sock结构,其中将刚才取得的inet_protosw中的pro 域赋值给sock的sk_prot域和sk_prot_creator.以及family域也被相应的赋值.  
        sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);  
        if (sk == NULL)  
            goto out;    
      
    ///这个函数中会初始化相应的socket中的写队列,读队列以及错误队列.并将sk指针和sock连接起来.而且还将初始化相应的定时器.  
      
        sock_init_data(sock, sk);  
    ///调用相应的初始化.  
      
        if (sk->sk_prot->init) {  
    ///其实也就是相对应4层协议的初始化函数,它会初始化一些协议相关的东西.  
            err = sk->sk_prot->init(sk);  
            if (err)  
                sk_common_release(sk);  
        }  
    out:  
        return err;  
    out_rcu_unlock:  
        rcu_read_unlock();  
        goto out;  
    }
    这里举个例子,来看一下tcp_v4_init_sock的实现,也就是tcp的初始化函数.

    static int tcp_v4_init_sock(struct sock *sk)  
    {  
        struct inet_connection_sock *icsk = inet_csk(sk);  
        struct tcp_sock *tp = tcp_sk(sk);  
      
        skb_queue_head_init(&tp->out_of_order_queue);  
    ///初始化定时器,也就是tcp的那3个定时器,write,delay以及keepalive定时器.  
        tcp_init_xmit_timers(sk);  
        tcp_prequeue_init(tp);   
    ///状态赋值.初始状态.  
        sk->sk_state = TCP_CLOSE;  
       
      
        return 0;  
    }  

    上面我们看到有两个新的结构inet_connection_sock以及tcp_sock.我们接下来就来看这两个结构. 

    inet_connection_sock也就是所有面向连接的协议的socket的相关信息.它的第一个域是inet_sock,因此我们可以很方便的进行转换.而tcp_sock 相当与inet_connection_sock得一个子类,保存有所有tcp相关的socket的信息.它的第一个域就是inet_connection_sock. 

    可以看到其实tcp_socket类似于inet_sock(前面的blog有介绍),都是保存了本层的相关的信息. 

    这里就不列出这两个结构了,内核中这两个结构的注释都是很详细的.. 


    在看sock_map_fd实现之前,我们先来看内核中socket类型的inode节点的实现: 

    这里看到,我们只要拥有了inode节点,通过containof宏我们就可以计算出socket的地址,从而就可以得到整个socket的信息了. 
    struct socket_alloc {  
        struct socket socket;  
        struct inode vfs_inode;  
    }; 
    而inode节点的赋值是在sock_alloc中实现的,而这个函数是在__sock_create中被调用的,也就是在init_cteate被调用之前. 

    static struct socket *sock_alloc(void)  
    {  
        struct inode *inode;  
        struct socket *sock;  
      
    ///新建一个inode,sock_mnt就是sock_init中被安装的mount点.  
        inode = new_inode(sock_mnt->mnt_sb);  
        if (!inode)  
            return NULL;  
    ///然后组合inode和socket结构.  
        sock = SOCKET_I(inode);  
    ///设置inode类型.  
        inode->i_mode = S_IFSOCK | S_IRWXUGO;  
        inode->i_uid = current->fsuid;  
        inode->i_gid = current->fsgid;  
    ///将sockets_in_use(也就是当前创建的socket)加一.  
        get_cpu_var(sockets_in_use)++;  
        put_cpu_var(sockets_in_use);  
        return sock;  
    }  
    然后我们来看sock_map_fd的实现.我们首先要知道,socket是没有open函数的,因此要通过vfs层的调用,必须要在create的时候,映射一个file结构,从而将句柄与这个file关联起来. 

    int sock_map_fd(struct socket *sock, int flags)  
    {  
        struct file *newfile;  
    ///找到一个可用的fd,并找到一个可用的file结构并返回.  
        int fd = sock_alloc_fd(&newfile, flags);  
      
        if (likely(fd >= 0)) {  
    ///初始化这个file结构.  
            int err = sock_attach_fd(sock, newfile, flags);  
      
            if (unlikely(err < 0)) {  
                put_filp(newfile);  
                put_unused_fd(fd);  
                return err;  
            }  
    ///将句柄和文件指针关联起来.  
            fd_install(fd, newfile);  
        }  
        return fd;  
    }  
    sock_alloc_fd实现比较简单,这里就不分析了. 
    就来看下sock_attach_fd的实现.: 
    这里要注意,内核通过把socket指针赋值给file的private_data,这样就可以通过句柄,在fdtable中得到file对象,然后轻松取得socket对象. 

    ///目录项的操作集合  
    static struct dentry_operations sockfs_dentry_operations = {  
        .d_delete = sockfs_delete_dentry,  
        .d_dname  = sockfs_dname,  
    };  
    ///文件的操作集合.这些函数最终调用的还是socket的ops域中的函数.而我们上面已经提过最终他们调用sock域的proto中的函数.  
    static const struct file_operations socket_file_ops = {  
        .owner =    THIS_MODULE,  
        .llseek =   no_llseek,  
        .aio_read = sock_aio_read,  
        .aio_write =    sock_aio_write,  
        .poll =     sock_poll,  
        .unlocked_ioctl = sock_ioctl,  
    #ifdef CONFIG_COMPAT  
        .compat_ioctl = compat_sock_ioctl,  
    #endif  
        .mmap =     sock_mmap,  
        .open =     sock_no_open,   /* special open code to disallow open via /proc */  
        .release =  sock_close,  
        .fasync =   sock_fasync,  
        .sendpage = sock_sendpage,  
        .splice_write = generic_splice_sendpage,  
        .splice_read =  sock_splice_read,  
    };  
      
    static int sock_attach_fd(struct socket *sock, struct file *file, int flags)  
    {  
        struct dentry *dentry;  
        struct qstr name = { .name = "" };  
    ///根据装载点的mnt_sb(super block)的root域来创建一个目录项.  
        dentry = d_alloc(sock_mnt->mnt_sb->s_root, &name);  
        if (unlikely(!dentry))  
            return -ENOMEM;  
    ///将sockfs的目录项操作集合赋值.  
        dentry->d_op = &sockfs_dentry_operations;  
        /* 
         * We dont want to push this dentry into global dentry hash table. 
         * We pretend dentry is already hashed, by unsetting DCACHE_UNHASHED 
         * This permits a working /proc/$pid/fd/XXX on sockets 
         */  
        dentry->d_flags &= ~DCACHE_UNHASHED;  
    ///将inode和目录项关联起来.  
        d_instantiate(dentry, SOCK_INODE(sock));  
      
        sock->file = file;  
    ///初始化文件对象,主要就是将socket_file_ops赋值给file结构的f_op域.  
        init_file(file, sock_mnt, dentry, FMODE_READ | FMODE_WRITE,  
              &socket_file_ops);  
        SOCK_INODE(sock)->i_fop = &socket_file_ops;  
        file->f_flags = O_RDWR | (flags & O_NONBLOCK);  
        file->f_pos = 0;  
    ///将sock赋值给private_data,这样我们就能通过file轻松获得socket结构(在后面会用到).  
        file->private_data = sock;  
      
        return 0;  
    }  
    下面就是sys_socket的流程图: 


     


    最终来总结一下.内核中,socket是作为一个伪文件系统来实现的,它在初始化时注册到内核,而每个进程的files_struct域保存了所有的句柄,包括socket的.一般的文件操作的话,内核直接调用vfs层的方法,然后会自动调用socket实现的相关方法.内核通过inode结构的imode域就可以知道当前的句柄所关联的是不是socket类型,这时遇到socket独有的操作,就通过containof方法,来计算出socket的地址,从而就可以进行相关的操作. 


    最后我们要注意的是,内核在调用相关操作都是直接调用socket的ops域,然后在ops域中调用相应的sock结构体中的sock_common域的skc_prot的操作集中的相对应的函数. 

    举个例子,假设现在我们使用tcp协议然后调用bind方法,内核会先调用sys_bind方法: 

    asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)  
    {   
                if (!err)  
                    err = sock->ops->bind(sock,  
                                  (struct sockaddr *)  
                                  &address, addrlen);   
    }  
    可以看到它调用的是ops域的bind方法.而这时我们的ops域是inet_stream_ops,来看它的bind方法: 
    int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)  
    {   
      
        /* If the socket has its own bind function then use it. (RAW) */  
        if (sk->sk_prot->bind) {  
            err = sk->sk_prot->bind(sk, uaddr, addr_len);  
            goto out;  
        }   
    }  
    它最终调用的是sock结构的sk_prot域(也就是sock_common的skc_prot域)的bind方法,而此时我们的skc_prot的值是tcp_prot,因此最终会调用tcp_prot的bind方法. 

    下面就是示意图: 



    PS:随便抱怨下,linux kernel的socket实现也太复杂了..不知道其他的操作系统的socket实现的怎么样..

    展开全文
  • linux内核中socket读取和接收缓冲区大小 1、socket内核缓冲区大小可用getsockopt获取 2、socket内核缓冲区大小可用setsockopt设置,缓冲区大小为设置2倍,具体设置代码入下 3、socket缓冲区设置最大为多...

    linux内核中socket读取和接收的缓冲区大小

    1、socket内核缓冲区大小可用getsockopt获取
    2、socket内核缓冲区大小可用setsockopt设置,缓冲区的大小为设置的值的2倍,具体设置代码入下
    3、socket缓冲区设置最大为多大呢?
    接收缓冲区最大为:/proc/sys/net/core/rmem_max
    接收缓冲区大小默认为:/proc/sys/net/core/rmem_default

    问题(望大神回答):
    1、发送缓冲区大小最大值跟默认值是多少呢?
    2、为什么新创建的socket的接收缓存区的大小,跟/proc/sys/net/core/rmem_default中设置的值不一样呢?
    3、内核中的socket缓冲区的大小,对于应用层编程有什么影响呢?
    4、接收跟发送的缓冲区的大小多少合适呢?


    #include <stdio.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    
    int main(void)
    {
            int socketFd;
    
            if ((socketFd = socket(PF_INET, SOCK_STREAM,0)) < 0)
            {
            /* 获取socket发送和接收缓冲区的大小 */
            optlen = sizeof(snd_size);
            err = getsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, &snd_size, &optlen);
            if (err < 0)
            {
    
                    printf("get send buff failed!\n");
            }
    
            optlen = sizeof(rcv_size);
            err = getsockopt(socketFd, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
            if (err < 0)
            { 
                    printf("get rev buff failed!\n");
            }   
                
            printf("senBuffLen:%d\n", snd_size);
            printf("recBuffLen:%d\n", rcv_size);
    
            /* 设置socket发送和接收缓冲区大小都为8K */
            snd_size = 8*1024;
            optlen = sizeof(rcv_size);
            err = setsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, &snd_size, optlen);
            if(err<0)
            {
                    printf("set send buff failed!\n");
            }
    
            rcv_size = 8*1024;
            optlen = sizeof(rcv_size);
            err = setsockopt(socketFd,SOL_SOCKET,SO_RCVBUF, (char *)&rcv_size, optlen);
            if(err<0){
                    printf("set rev buff failed!\n");
            }
    
            /* 获取socket接收和发送缓冲区大小 */
            optlen = sizeof(snd_size);
            err = getsockopt(socketFd, SOL_SOCKET, SO_SNDBUF, &snd_size, &optlen);
            if (err < 0)
            {
                    printf("get send buff failed!\n");
            }
    
            optlen = sizeof(rcv_size);
            err = getsockopt(socketFd, SOL_SOCKET, SO_RCVBUF, &rcv_size, &optlen);
            if (err < 0)
            {
                    printf("get rev buff failed!\n");
            }
    
            printf("senBuffLen:%d\n", snd_size);
            printf("recBuffLen:%d\n", rcv_size);
    
    
            return 0;
    }

     

    展开全文
  • 内核中socket address family注册过程

    千次阅读 2013-07-29 00:45:04
    以AF_BLUETOOTH为例,梳理内核中socket address family注册过程。 linux内核启动之时,会初始化各subsystem(子系统),bluetooth就是这样一个子系统之一 bluetooth/af_bluetooth.c  subsys_initcall(bt_init)...
  • 内核中的UDP socket流程(1)

    千次阅读 2013-06-23 18:17:41
    内核中的UDP socket流程(1)  相对于TCP,UDP协议要简单的多。所以我决定由简入繁,先从UDP协议入手。 前一遍文章已经确定了struct sk_buff被用于socket的接受和发送缓冲。那么为了摸清linux发送...
  • 我们已经探讨过了从内核的task_struct到socket的钩子函数过程,如果你读到这里没有看到或者没有明白,请看前边章节中的read和write的分析内容。好了,我们还是直接从socket.c中的文件的钩子函数看起 static...
  • 内核中的UDP socket流程(2)——API “sys_socket” 作者:gfree.wind@gmail.com 原文:http://blog.chinaunix.net/u3/116859/showart.php?id=2445122 前面已经列出了UDP常用的4个API,那么下面从第一个API “sys...
  • 内核中的UDP socket流程(6)——sendto作者:gfree.wind@gmail.com 原文:http://blog.chinaunix.net/space.php?uid=23629988&do=blog&id=85912 现在开始新的API sendto,那么就重新回到了socket.c文件...
  • 详细分析了linux内核中sock和socket数据结构含义
  • https://www.cnblogs.com/hyd-desert-camel/p/3536341.htmldesert-camellinux内核中socket的创建过程源码分析(详细分析)1三个相关数据结构.关于socket的创建,首先需要分析socket这个结构体,这是整个核心。104...
  • linux内核中socket完全理解

    千次阅读 2019-02-12 19:41:24
    如果你知道Linux系统进程间通信方式,就应该知道套接字也是其中一种。但套接字特别之处在于它不仅可以用来实现同一台主机上进程间通信,还可以用来实现主机间进程间通信。通信双方各自打开一个套接字,...
  • 引起select返回socket"准备好"的条件有以下三个说明:(TCP V2的图16.52) ...对这样的socket的读操作不会阻塞,并返回一个大于0的值(即:准备好读入的数据的字节数).我们可以用socket选项SO_RCVLOWA
  • 内核中的UDP socket流程(3)——sock_create作者:gfree.wind@gmail.com 原文:http://blog.chinaunix.net/space.php?uid=23629988&do=blog&id=85909 由于种种原因,工作的,私人的,学习停了几天。周末...
  • 内核中的UDP socket流程(5)——inet_create 作者:gfree.wind@gmail.com 原文:http://blog.chinaunix.net/space.php?uid=23629988&do=blog&id=85911 进入函数inet_create static ...
  • 在漫长地分析完socket的创建源码后,发现一片浆糊,所以特此总结,我博客同时有另外一篇详细源码分析,内核版本为3.9,建议在阅读本文后若还有兴趣再去看另外一篇博文。绝对不要单独看另外一篇。 一:调用链...
  • 内核中的UDP socket流程(4)——sock_create作者:gfree.wind@gmail.com 原文:http://blog.chinaunix.net/space.php?uid=23629988&do=blog&id=85910 又懒了2天,继续sock_create /...
  • 在linux内核模块编写socket程序时,用bind绑定了一个端口后,发送数据可以成功。 但是端口不能及时释放,下次加载模块时,会出现地址被占用提示。 需要等一会后,这个端口才会释放。 但是为了能马上再绑定...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,054
精华内容 1,221
关键字:

内核中的socket