简析socket的实现

先介绍一下Socket的来源。Socket本来是加利福尼亚大学伯克利分校为BSD操作系统(UNIX的一种)开发的进程间通信机制,它既可用于同一计算机之上的进程间通信,也可用于网络环境不同计算机之间的进程间通信。

  本文并不详细讨论socket的具体用法,而是首先从使用socket的角度出发,来简单探讨一下在Linux环境中,socket函数是如何从应用程序空间进入内核,并被内核所支持的。

 探讨之前我们需要有一个简单的认识:我们平时在应用层编程时所使用的socket相关的函数,是由glibc(或其他C库,如果你不用GNUC库的话)来支持的,而glibc中的socket函数的实现则主要基于内核所提供的相关系统调用。

 

先来简单介绍一下常用的Socket接口函数:

1.创建套接口接口socket函数。函数原型为:

int socket(int domain, int type, int protocol)

这个函数有三个参数,第一个参数指定协议族,如AF_INET(IPv4协议)AF_INET6 (IPv6协议)AF_LOCAL(Unix域协议)。第二个参数为套接口类型。第三个参数指定协议,也可以取0Socket函数成功时返回一个套接口文件描述符。

 

2.绑定套接口接口 bind函数。函数原型为:

int bind(int sockfd, struct sockaddr *my_addr, int addrlen)

其中各项参数的含义为:

sockfd:由socket调用返回的文件描述符

addrlen:就是sizeof(sockaddr), 套接字地址结构的长度

my_addr:一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;sockaddr的定义如下:

struct sockaddr{

  unsigned short sa_family; /* 地址族, AF_xxx */

   char sa_data[14]; /* 14 字节的协议地址 */   

};

 

另外还有一种用于Internet的套接字地址结构类型:

struct sockaddr_in {

   short int sin_family; /* 地址族 */

   unsigned short int sin_port; /* 端口号 */

   struct in_addr sin_addr; /* IP地址 */

   unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */

};

bind函数返回值表明操作成功或失败:成功返回0,出错返回-1

 

3.建立连接接口connect函数

面向连接的客户端程序使用connect函数来配置socket并与远端服务器建立一个TCP连接,其函数原型为:

int connect(int sockfd, struct sockaddr *serv_addr,int addrlen)

其中sockfdsocket函数返回的socket描述符;serv_addr是包含远端主机IP地址和端口号的指针;addrlen是远端地址结构的长度。connect函数在出现错误时返回-1,并且设置errno为相应的错误码。编写客户端程序无须调用bind()

 

4.监听接口 listen函数

listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。

int listen(int sockfd, int backlog)

 

5. 接受请求接口accept函数

accept函数由TCP服务器端调用,用来接受从客户端来的请求。如果没有请求,则该函数自行阻塞,直到有请求为止。

int accept(int sockfd, struct sockaddr *cliaddr,socklen_t *addrlen)

 

6. 关闭套接字接口close函数

int close( int sockfd)

closeTCP套接字sockfd的默认的操作是将其标识为已关闭并立即返回;这时套接字描述符就不能再被进程使用了,也不能作为readwrite的参数了。

 

关于这些函数如何使用的资料很多,本文不作讨论,我们重点关注这些函数具体是如何工作的。

由于这些socket函数是由glibc提供的,我们先从glibc开始寻找。下面的探讨基于glibc 2.6的源代码。我们可以在glibc 源代码的include/sys/socket.h文件中找到上述socket函数的声明,这些函数的真正实现比较难找,对于x86体系来说,相关源文件在sysdeps/unix/sysv/linux/i386/socket.S,这是用汇编实现的,用来从用户空间进入名为socketcall的系统调用,并传递参数,下面是相关汇编代码:

.globl __socket

ENTRY (__socket)

#if defined NEED_CANCELLATION && defined CENABLE

       SINGLE_THREAD_P

       jne 1f

#endif

 

       /* Save registers.  */

       movl %ebx, %edx

       cfi_register (3, 2)

 

       movl $SYS_ify(socketcall), %eax     /* System call number in %eax.  */

 

       /* Use ## so `socket' is a separate token that might be #define'd.  */

       movl $P(SOCKOP_,socket), %ebx      /* Subcode is first arg to syscall.  */

       lea 4(%esp), %ecx        /* Address of args is 2nd arg.  */

 

        /* Do the system call trap.  */

       ENTER_KERNEL

 

       /* Restore registers.  */

       movl %edx, %ebx

       cfi_restore (3)

 

       /* %eax is < 0 if there was an error.  */

       cmpl $-125, %eax

       jae SYSCALL_ERROR_LABEL

 

       /* Successful; return the syscall's value.  */

L(pseudo_end):

       ret

 

注意上面所用的汇编代码采用的是AT&T格式(通常学校所教的是Intel汇编),这是Linux中对x86体系常用的汇编格式。glibc中一般直接采用这种汇编代码来进入内核访问系统调用,而不是_syscalln()格式的宏。

上面代码的作用是进入内核来访问名为socketcall的系统调用,在内核代码中,socketcall系统调用的对应代码在net/socket.c文件中。下面的代码取自Linux内核版本2.26.11

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;

}

上面的switch/case对应前面socket函数组中的各个函数。这样对于用户空间的一组socket函数,实际上只用到了socketcall一个系统调用,通过不同的参数来进行区分,而进入内核中的实现则可发现,对于不同的参数,由switch/case中不同的case再分别进入不同的内核函数。

到这里我们一路找到了socket函数组所对应的内核代码,更详细的socket在内核中的实现的将另文分析.

更多文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值