Linux网络协议栈 -- socket创建(1)

来源:互联网 发布:三位一体 for mac 编辑:程序博客网 时间:2024/06/06 05:07

内核版本:2.6.12 


一、系统总入口

 

Linux 内核为所有的与 socket 有关的操作的 API,提供了一个统一的系统调用入口,其代码在net/socket.c中: 


asmlinkage long sys_socketcall(int call, unsigned long __user *args) 

        unsigned long a[6]; 
        unsigned long a0,a1; 
        int err; 
 
        if(call<1||call>SYS_RECVMSG) 
                return -EINVAL; 
 
        /* copy_from_user should be SMP safe. */ 
        if (copy_from_user(a, args, nargs[call])) 
                return -EFAULT; 
                 
        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 = sys_accept(a0,(struct sockaddr __user *)a1, (int __user *)a[2]); 
                        break; 
                case SYS_GETSOCKNAME: 
                        err = sys_getsockname(a0,(struct sockaddr __user *)a1, (int __user *)a[2]); 
                        break; 
                case SYS_GETPEERNAME: 
                        err = sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); 
                        break; 
                case SYS_SOCKETPAIR: 
                        err = sys_socketpair(a0,a1, a[2], (int __user *)a[3]); 
                        break; 
                case SYS_SEND: 
                        err = sys_send(a0, (void __user *)a1, a[2], a[3]); 
                        break; 
                case SYS_SENDTO: 
                        err = sys_sendto(a0,(void __user *)a1, a[2], a[3], 
                                         (struct sockaddr __user *)a[4], a[5]); 
                        break; 
                case SYS_RECV: 
                        err = sys_recv(a0, (void __user *)a1, a[2], a[3]); 
                        break; 
                case SYS_RECVFROM: 
                        err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3], 
                                            (struct sockaddr __user *)a[4], (int __user *)a[5]); 
                        break; 
                case SYS_SHUTDOWN: 
                        err = sys_shutdown(a0,a1); 
                        break; 
                case SYS_SETSOCKOPT: 
                        err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); 
                        break; 
                case SYS_GETSOCKOPT: 
                        err = sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); 
                        break;                 case SYS_SENDMSG: 
                        err = sys_sendmsg(a0, (struct msghdr __user *) a1, a[2]); 
                        break; 
                case SYS_RECVMSG: 
                        err = sys_recvmsg(a0, (struct msghdr __user *) a1, a[2]); 
                        break; 
                default: 
                        err = -EINVAL; 
                        break; 
        } 
        return err; 
}
 

首先调用 copy_from_user将用户态参数拷贝至数组 a。但是问题在于,每个被调用的 API 的参数不尽相同,那么每次拷贝的字节在小如果断定? 来看其第三个参数 nargs[call],其中 call 是操作码,后面有个大大的 switch...case 就是判断它。对应的操作码定义在 include/linux/net.h:

 

#define SYS_SOCKET        1                /* sys_socket(2)                */ 
#define SYS_BIND        2                /* sys_bind(2)                        */ 
#define SYS_CONNECT        3                /* sys_connect(2)                */ 
#define SYS_LISTEN        4                /* sys_listen(2)                */ 
#define SYS_ACCEPT        5                /* sys_accept(2)                */ 
#define SYS_GETSOCKNAME        6                /* sys_getsockname(2)                */ 
#define SYS_GETPEERNAME        7                /* sys_getpeername(2)                */ 
#define SYS_SOCKETPAIR        8                /* sys_socketpair(2)                */ 
#define SYS_SEND        9                /* sys_send(2)                        */ 
#define SYS_RECV        10                /* sys_recv(2)                        */ 
#define SYS_SENDTO        11                /* sys_sendto(2)                */ 
#define SYS_RECVFROM        12                /* sys_recvfrom(2)                */ 
#define SYS_SHUTDOWN        13                /* sys_shutdown(2)                */ 
#define SYS_SETSOCKOPT        14                /* sys_setsockopt(2)                */ 
#define SYS_GETSOCKOPT        15                /* sys_getsockopt(2)                */ 
#define SYS_SENDMSG        16                /* sys_sendmsg(2)                */ 
#define SYS_RECVMSG        17                /* sys_recvmsg(2)                */[/code] 
 

而数组 nargs则根据操作码的不同,计算对应的参数的空间大小:

 

/* Argument list sizes for sys_socketcall */ 
#define AL(x) ((x) * sizeof(unsigned long)) 
static unsigned char nargs[18]={AL(0),AL(3),AL(3),AL(3),AL(2),AL(3), 
                                 AL(3),AL(3),AL(4),AL(4),AL(4),AL(6), 
                                 AL(6),AL(2),AL(5),AL(5),AL(3),AL(3)}; 
#undef AL
 

当拷贝完成参数后,就进入一个 switch...case...判断操作码,跳转至对应的系统接口。 


二、 sys_socket 函数

 
操作码 SYS_SOCKET 是由 sys_socket()实现的: 
asmlinkage long sys_socket(int family, int type, int protocol) 

        int retval; 
        struct socket *sock; 
 
        retval = sock_create(family, type, protocol, &sock); 
        if (retval < 0) 
                goto out; 
 
        retval = sock_map_fd(sock); 
        if (retval < 0) 
                goto out_release; 
 
out: 
        /* It may be already another descriptor 8) Not kernel problem. */ 
        return retval; 
 
out_release: 
        sock_release(sock); 
        return retval; 
}
 
在分析这段代码之间,首先来看,创建一个 Socket,对内核而言,究竟意味着什么?究竟需要内核干什么事? 
 
当用户空间要创建一个 socke 接口时,会调用 API 函数: 
int socket(int domain, int type, int protocol); 
 
函数,其三个参数分别表示协议族、协议类型(面向连接或无连接)以及协议。 
 
对于用户态而言,一个 Scoket,就是一个特殊的,已经打开的文件。为了对 socket抽像出文件的概念,内核中为 socket 定义了一个专门的文件系统类型 sockfs: 
static struct vfsmount *sock_mnt; 
 
static struct file_system_type sock_fs_type = { 
        .name =                "sockfs", 
        .get_sb =        sockfs_get_sb, 
        .kill_sb =        kill_anon_super, 
};
 
在模块初始化的时候,安装该文件系统:  
void __init sock_init(void) 

        …… 
        register_filesystem(&sock_fs_type); 
        sock_mnt = kern_mount(&sock_fs_type);         
}
 
有了文件系统后,对内核而言,创建一个 socket, 就是在 sockfs 文件系统中创建一个文件节点(inode),并建立起为了实现 socket 功能所  需的一整套数据结构,包括 struct inode 和 struct socket 结构。 struct socket 结构在内核中,就代表了一个"Socket",当一个 struct socket 数据结构被分配空间后,再将其与一个已打开的文件“建立映射关系”。这样,用户态就可以用抽像的文件的概念来操作socket了——当然,由  于网络的特殊性,至少就目前而言,这种抽像,并不如其它模块的抽像那么完美。 
 

文件系统 struct vfsmount 中有一个成员指针 mnt_sb 指向该文件系统的超级块,而超级块结构 struct super_lock 有一个重要的成员 s_op 指向了超级块的操作函数表,其中有函数指针 alloc_inode()即为在给定的超级块下创建并初始化一 个新的索引节点对像。也就是调用: 

sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);

当然,连同相关的处理细节一起,这一操作被层层封装至一个上层函数 new_inode()。 
 
那如何分配一个 struct socket 结构呢?如前所述,一个 socket 总是与一个inode 密切相关的。当然,在 inode 中,设置一个 socket 成员,是完全可行的,但是  这貌似浪费了空间——毕竟,更多的文件系统没有 socket 这个东东。所以,内核引入了另一个 socket_alloc 结构: 


struct socket_alloc { 

        struct socket socket; 
        struct inode vfs_inode; 

};


显而易见,该结构实现了 inode 和 socket 的封装。已经一个 inode,可以通过宏 SOCKET_I 来获取与之对应的 socket: 
sock = SOCKET_I(inode); 
 
static inline struct socket *SOCKET_I(struct inode *inode) 

        return &container_of(inode, struct socket_alloc, vfs_inode)->socket; 

 

但是,这样做,也同时意味着,在分配一个 inode 后,必须再分配一个 socket_alloc结构,并实现对应的封装。否则,container_of 又能到哪儿去找到 socket 呢?现在来简要地看一个这个流程——这是文件系统安装中的一个重要步骤:

 

struct vfsmount *kern_mount(struct file_system_type *type) 

        return do_kern_mount(type->name, 0, type->name, NULL); 
}
 
struct vfsmount * 
do_kern_mount(const char *fstype, int flags, const char *name, void *data) 

        struct file_system_type *type = get_fs_type(fstype); 
        struct super_block *sb = ERR_PTR(-ENOMEM); 
                …… 
                sb = type->get_sb(type, flags, name, data); 
                …… 
               mnt->mnt_sb = sb; 
               …… 

 
do_kern_mount 函数中,先根据注册的文件系统类型,调用 get_fs_type 获取之,也就是我们之前注册的 sock_fs_type,然后调用它的 get_sb 成员函数指针,获取相应的超级块 sb。最后,调置文件系统的超级块成员指针,使之指向对应的值。 这里 get_sb函数指针,指向之前初始化的 sockfs_get_sb()函数。 


static struct super_block *sockfs_get_sb(struct file_system_type *fs_type, 

        int flags, const char *dev_name, void *data) 

        return get_sb_pseudo(fs_type, "socket:", &sockfs_ops, SOCKFS_MAGIC); 

 
注意其第三个参数 sockfs_ops,它封装了 sockfs 的功能函数表: 
static struct super_operations sockfs_ops = { 
        .alloc_inode =        sock_alloc_inode, 
        .destroy_inode =sock_destroy_inode, 
        .statfs =        simple_statfs, 
};
 
struct super_block * 
get_sb_pseudo(struct file_system_type *fs_type, char *name, 
        struct super_operations *ops, unsigned long magic) 

        struct super_block *s = sget(fs_type, NULL, set_anon_super, NULL); 
                …… 
         
                s->s_op = ops ? ops : &default_ops; 

}


这里就是先获取/分配一个超级块,然后初始化超级块的各成员,包括 s_op,我们前面提到过它,它封装了对应的功能函数表。这里 s_op 自然就指向了 sockfs_ops。那前面提到的 new_inode()函数分配 inode 时调用的:

 

sock_mnt->mnt_sb->s_op->alloc_inode(sock_mnt->mnt_sb);


这个 alloc_inode 函数指针也就是 sockfs_ops的 sock_alloc_inode()函数——转了一大圈,终于指到它了。 来看看 sock_alloc_inode 是如何分配一个 inode 节点的: 


static struct inode *sock_alloc_inode(struct super_block *sb) 


        struct socket_alloc *ei; 
        ei = (struct socket_alloc *)kmem_cache_alloc(sock_inode_cachep, SLAB_KERNEL); 
        if (!ei) 
                return NULL; 
        init_waitqueue_head(&ei->socket.wait); 
         
        ei->socket.fasync_list = NULL; 
        ei->socket.state = SS_UNCONNECTED; 
        ei->socket.flags = 0; 
        ei->socket.ops = NULL; 
        ei->socket.sk = NULL; 
        ei->socket.file = NULL; 
        ei->socket.flags = 0; 
 
        return &ei->vfs_inode; 

 

函数先分配了一个用于封装 socket 和 inode 的 ei,然后在高速缓存中为之申请了一块空间。这样,inode 和 socket 就同时都被分配了。接下来初始化 socket 的各个成员,这些成员,在后面都会一一提到。 


/*
*  struct socket - general BSD socket 
*  @state: socket state (%SS_CONNECTED, 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 
*  @type: socket type (%SOCK_STREAM, etc) 
*/ 
struct socket { 
        socket_state                state; 
        unsigned long                flags;         struct proto_ops        *ops; 
        struct fasync_struct        *fasync_list; 
        struct file                *file; 
        struct sock                *sk; 
        wait_queue_head_t        wait; 
        short                        type; 
}; 
 
OK,至目前为止,分配 inode、socket 以及两者如何关联,都已一一分析了。 最后一个关键问题,就是如何把 socket 与一个已打开的文件,建立映射关系。 
 

在内核中,用 struct file结构描述一个已经打开的文件,指向该结构的指针内核中通常用 file或 filp来描述。我们知道,内核中,可以通过全局项 current 来获得当  前进程,它是一个 struct task_struct类型的指针。tastk_struct 有一个成员: 


struct files_struct *files; 


指向一个已打开的文件。当然,由于一个进程可能打开多个文件,所以,struct files_struct 结构有 struct file * fd_array[NR_OPEN_DEFAULT]; 
成员,这是个数组,以文件描述符为下标,即 current->files->fd[fd],可以找到与当前进程指定文件描述符的文件。 
 

有了这些基础,如果要把一个 socket 与一个已打开的文件建立映射,首先要做的就是为 socket分配一个struct file,并申请分配一个相应的文件描述符fd。因为socket并不支持open方法(前面说socket的文件界面的抽像并不完美,这应该是一个佐证 吧?),所以不能期望用户界面通过调用 open() API来分配一个 struct file,而是通过调用get_empty_filp 来获取:

 

struct file *file = get_empty_filp();
 
同样地: 
int fd; 
fd = get_unused_fd();
获取一个空间的文件描述符 
 
然后,让 current 的 files指针的 fd 数组的 fd 索引项指向该 file: 
 
void fastcall fd_install(unsigned int fd, struct file * file) 

        struct files_struct *files = current->files; 
        spin_lock(&files->file_lock); 
        if (unlikely(files->fd[fd] != NULL)) 
                BUG(); 
        files->fd[fd] = file; 
        spin_unlock(&files->file_lock); 
}
 
OK,做到这一步,有了一个文件描述符 fd 和一个打开的文件 file,它们与当前进程相连,但是好像与创建的socket并无任何瓜葛。要做的映射还是没  有进展。 struct file或文件描述述fd或current都没有任何能够与 inode或者是 socket 相关的东东。这需要一个中间的桥梁,目录项:struct dentry结构。 因为一个文件都有与其对应的目录项: 


struct file { 

        struct list_head        f_list; 
        struct dentry                *f_dentry; 
…… 
 
而一个目录项: 
struct dentry { 
…… 
        struct inode *d_inode;                /* Where the name belongs to - NULL is 
                                         * negative */ 
 
d_inode 成员指向了与之对应的 inode节点…… 
 

而之前已经创建了一个 inode 节点和与之对应的 socket。 所以,现在要做的,就是: “先为当前文件分配一个对应的目录项,再将已创建的 inode节点安装至该目录项” 这样,一个完成的映射关系: 进程、文件描述符、打开文件、目录项、inode节点、socket就完整地串起来了。 


现在可以来看套接字的创建过程了: 
 
asmlinkage long sys_socket(int family, int type, int protocol) 

        int retval; 
        struct socket *sock; 
 
        retval = sock_create(family, type, protocol, &sock); 
        if (retval < 0) 
                goto out; 
 
        retval = sock_map_fd(sock); 
        if (retval < 0) 
                goto out_release; 
 
out: 
        /* It may be already another descriptor 8) Not kernel problem. */ 
        return retval; 
 
out_release:         sock_release(sock); 
        return retval; 

 
int sock_create(int family, int type, int protocol, struct socket **res) 

        return __sock_create(family, type, protocol, res, 0); 
}

原创粉丝点击