先介绍一下Socket的来源。Socket本来是加利福尼亚大学伯克利分校为BSD操作系统(UNIX的一种)开发的进程间通信机制,它既可用于同一计算机之上的进程间通信,也可用于网络环境不同计算机之间的进程间通信。
本文并不详细讨论socket的具体用法,而是首先从使用socket的角度出发,来简单探讨一下在Linux环境中,socket函数是如何从应用程序空间进入内核,并被内核所支持的。
探讨之前我们需要有一个简单的认识:我们平时在应用层编程时所使用的socket相关的函数,是由glibc(或其他C库,如果你不用GNU的C库的话)来支持的,而glibc中的socket函数的实现则主要基于内核所提供的相关系统调用。
先来简单介绍一下常用的Socket接口函数:
1.创建套接口接口socket函数。函数原型为:
int socket(int domain, int type, int protocol) |
这个函数有三个参数,第一个参数指定协议族,如AF_INET(IPv4协议),AF_INET6 (IPv6协议),AF_LOCAL(Unix域协议)。第二个参数为套接口类型。第三个参数指定协议,也可以取0。Socket函数成功时返回一个套接口文件描述符。
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) |
其中sockfd是socket函数返回的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) |
close对TCP套接字sockfd的默认的操作是将其标识为已关闭并立即返回;这时套接字描述符就不能再被进程使用了,也不能作为read和write的参数了。
关于这些函数如何使用的资料很多,本文不作讨论,我们重点关注这些函数具体是如何工作的。
由于这些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在内核中的实现的将另文分析.