也许学过从事过网络编程的人都知道socket是什么,表示什么?socket的英文原义是“孔”或“插座。但我们用网络术语将它称为“套接字”(见Linux网络编程),但是我习惯叫“套接口”,可能是受Unix网络编程的影响。里面是这样解释的:首先Socket作为网络API之一,跟XTI一样,是应用层或其他协议层访问接口,其次具体使用的套接口是与Unix管道某端口类似的机制,应用程序和内核(实际是内核实现的网络堆栈)可一通过它来通信,也即进程间的套接字通信;这里我们不讨论怎么称呼他,我们在乎的是如何很好的使用它。
为什我们称为网络堆栈?我想这得益于他的层次结构来的吧,我们知道ISO把他分为7层:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。但是实际应用中我们一般只会用会用到四层也即典型的TCP/IP协议(Transmission Control Protocol/Internet Protocol)了:应用层、传输层、网络层、网络接口层。如图:
这里我就不讲解每次的用处,懂网络编程的朋友估计也不需要我去赘述。下面我将分析有应用层至内核这些机制进行互联互通的。我就成为:Linux网络编程深究
Linux网络编程深究
上图我们还可以细分:
应用层处理具体应用(FTP、Telent、HTTP等)的细节他也不会去管他下面一层(TCP/UDP)的通信细节。而下面几层也不回去管上面怎么实现应用的,他们只是做好下层的所有通信细节(接/发、等待确认、给无序到达的数据排序,计算与校验和等),上层我们一般称之为用户进程,下面XTI隔界的则属于内核一部分。从上图我们可以看出,应用成也可以直接绕过TCP/UDP而直接使用IPv4/6,这就是原始套接口,原是套接口可以直接读写数据链路层的帧。支持该机制的操作系统大概为原是套接口提供了如下三种功能:
1、有了原始套接口,进程可以读写ICMPv4,IGMPv4和ICMPv6等分组。举例来说:ping就是使用原始套接口发送ICMP回射请求并接收ICMP回射应答。多播路由守护程序mrouted也使用原始套接口发送和接收IGMPv4分组。这个能力还使得使用ICMP或IGMP构造的应用程序能够完全作为用户进程处理,而不必往内核中添加额外代码。
2、有了原始套接口,进程可以读写内核不处理其协议字段的IPv4数据报。大多数内核仅仅处理该字段值为1(ICMP),2(IGMP),6(TCP)和17(UDP)的数据报。然而为协议字段定义的值还有不少。
3、有了原始套接口,进程还可以使用IP_HDRINCL套接口选项执行构造IPv4头部。这个能力可用于构造TCP或UDP分组等。
我们可以通过给套接口Socket函数的第二个参数设定为SOCK_RAW既要告知内核返回一个原是套接口结构。这不是我们的话重点,也就是将到哪提到那我就随便提下。
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 #include
9 #include
10 #include
11 #include
12 #include
13 #include
14 #include
15 #include
16 #include
17 #include
18 #include
19 #include
#define SERVPORT 3333
#define BACKLOG 10
#define MAX_CONNECTED_NO 10
#define MAXDATASIZE 1000
这里就讲下 SERVPORT和BACKLOG
SERVPORT :这是我们自定义的端口号,1-1024号默认被系统占用,为了不发生冲突我们还是自定义一个端口号。那么为什要定义端口号呢?我们知道一台服务器某时刻可能会有各种网络服务进程在运行,但是我们客户进程千里迢迢来访怎么才能找到命中的哪个他呢?这就得通过匹配了(端口),看了客户服务端程序后才会涣然大悟的。一个典型的例子就是:服务器进程就相当于我们的插口板,上面有各种各样的插口(Port)(两个脚的,三个脚的等),网线和客户程序就相当于我们的用电器,用电器要得到服务(上电)我们就必须使用用电器连出的线来插入到插电板上,其中要插入插口板的一端我们可以把它当作Socket。所以我们要知道该往几个脚的插口上插,必须要知道电路板上是否有匹配的插口,有就插入,也即形成了通路,便可得到服务了。
BACKLOG :这个讲起来有点抽象,我们知道TCP是提供可靠的通讯的,所以在连接之前必须先发送(SYN j)连接请求,后服务端做出相应,然后发送(SYN K,ACK y+1)确认信号,接着客户发送(ACK K+1)若无异常服务器端便会采取相应措施处理。这里内核会为到来的客户创建两个队列:
1、未完成等待队列:每个SYN分节对应其中一项,已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程,这些套接口处于SYN_RCVD状态;
2、已完成连接队列:每个完成TCP三次握手过程的客户对应其中某一项。
所以这个BACKLOG 就是这两套队列的最大长度,一般我们可以这样说,它是由未完成连接构成的队列可能增长到的最大长度,即指定某套接口上内核为之排队的最大(已完成)连接数,口号内的我们一般会将其忽略。但若你是要参加面是的话,跟他说从头到尾的解释一遍后,面试官会对你另眼相看。为什么要加已完成呢?这是因为当年的SYN泛滥的新型攻击,当时某黑客利用这机制,不断向服务器以高速率的方式发送SYN请求,导致服务器一直忙一SYN的响应,最主要是服务器内核第一条队列被非法的SYN占有,使得合法的SNY连不上。所以规定为已完成的数目就可以避免此攻击了(题外话)。
我们把头文件声明好了,接下来就讲解Socket系统调用函数的怎么创建返回一个sockfd给应用程序的吧:
int socket (int family , int type , int protocol)
Family:表示协议类型
Type :表示要创建的套接口的类型
Protocol:看字面意思就知道是要创建的套接口遵守的协议了。但是他表示前面family中的 一种,通常family里只有一中的,所以它通常为0
因为前面我们讲过,我们一般的网络通讯都是用TCP/IP的,所以通常该函数几个参数我们可以规定了,就当是默认的吧,当初我就是这样理解该函数的。
Int listenfd =socket(AF_INET ,SOCK_STREAM ,0)
其实该函数是系统调用函数,它类似于open、read,write等函数通过系统调用函数:sys_open,sys_read,sys_write实现相应的操作。但是在Linux中socket是通过sys_socketcall来逐步完成的:
asmlinkage long sys_socketcall(int call, unsigned long *args);
{
Int er;
switch (call)
{
case SYS_SOCKET:
er = verify_area(VERIFY_READ , args , sizeof(long));
If (er)
return er;
return (sock_socket( get_fs_long(args+0),get_fs_long(args+1),get_fs_long(args+2))) ;
break;
case SYS_BIND:
er = verify_area(VERIFY_READ , args , sizeof(long));
If (er)
return er;
return (sock_bind( get_fs_long(args+0),(struct sockaddr *)get_fs_long(args+1),get_fs_long(args+2))) ;
case SYS_ACCEPT:
er = verify_area(VERIFY_READ , args , sizeof(long));
If (er)
return er;
return (sock_bind( get_fs_long(args+0),(struct sockaddr *)get_fs_long(args+1),get_fs_long(args+2))) ;
....
}
}
Sys_socketcall()函数有两个参数,第一个参数表示被调用的应用层接口函数,第二个指向被调用函数所需的参数,用户程序进行系统调用时传入的参数将原封不动的传递给内核网络堆栈相应底层函数,socket为sock_socket();那具体过程是如何传递的呢?我们以accept为例来讲述
一、
*/inux/accept.S
1.#define socket accept
2.#deifine _socket _lib_accept
3.#define NARGS 3
4.#include
二、socket.S
15.#include
16.#...........
17.#define P(a,b) P2(a,b)
18.#define P2(a,b) a##b //<=>P2 = ab ##表连接字符
19..text
26.#ifndef _socket
27.Ifndef NO_WEAK_ALIAS
28.#define _socket P(_,socket) //<=>P=_socket
29.#else
30.#define _socket socket
31.#enfif
32.#endif
33..globl _socket
34.ENTRY (_socket)
36.movl �x,�x
37.movl $SYS_Ifyc(socketcall),�x //eax保存的是本次系统调用号
38.Movl $P(SOCKOP_, socket) ,�x //也即把子调用号存入ebx中
39.Lea 4(%esp),�x //当前被调用函数的参数存入ecx中
40.Int $0x80
41.Movl �x ,�x //内陷后,恢复ebx
42.Cmpl $-125 , �x
....
其中 SYS_ifc是这样定义的:
#ifdef _STDC_
#define SYS_ify(systemcall_name)
SYS_##syscall_name
#else
#define SYS_ify(syscall_name) SYS_syscall_name
#endif
所以经过预处理后socket.S中37行就很好理解了:(等价于)
Movl $SYS_socketcall ,�x
三、
那socket.S中的SYS_socketcall从哪来的呢?我们通过查找/usr/include/bits/syscall.h可以看到:
286 #define SYS_sigsuspend __NR_sigsuspend
287 #define SYS_socketcall __NR_socketcall
288 #define SYS_splice __NR_splice
289 #define SYS_ssetmask __NR_ssetmask
即把SYS_socketcall定义为__NR_socketcall 了。而__NR_socketcall我们可以在/include/asm/unistd_32.h中看到:
#define __NR_fstatfs 100
#define __NR_ioperm 101
#define __NR_socketcall 102
#define __NR_syslog 103
#define __NR_setitimer 104
换句话说就是把socket.S中37行的目的就是为了将sys_socketcall对应的系统调用号(102)被赋值予eax中,从而使得套接字系统调用进入到相应的入口函数中,那么调用系统函数的参数是怎么样传递的呢?因为系统调用中参数从用户态向内核态的传递是通过寄存器完成的,eax存放的是当前的系统调用号,ebx存放的是改掉用好对应的第一个参数,ecx存放的是该调用号对应的第二个,Ecx为第三个...在系统调用过程中编译器仅从堆栈中获取数据,而不会从寄存器中获取任何数据,所以在进入system_call前,用户程序会将参数放置相应寄存器中system_call函数执行时就会将这些寄存器中的数据全部压栈,因此系统调用服务例程便可从被system_call函数压入的堆栈中方便的获取数据,对数据的修改也在堆栈中进行,所以当系统调用结束后用户程序便可直接从堆栈中获取(被修改过的)数据。那么SOCKOP_socket代表什么呢?
1 . // glibc-2.0.111\sysdeps\unix\sysv\linux\socketcall.h
2 ……
3 #define SOCKOP_socket 1
4 #define SOCKOP_bind 2
5 #define SOCKOP_connect 3
6 #define SOCKOP_listen 4
7 #define SOCKOP_accept 5
8 #define SOCKOP_getsockname 6
9 #define SOCKOP_getpeername 7
10 #define SOCKOP_socketpair 8
11 #define SOCKOP_send 9
12 #define SOCKOP_recv 10
13 #define SOCKOP_sendto 11
14 #define SOCKOP_recvfrom 12
15 #define SOCKOP_shutdown 13
16 #define SOCKOP_setsockopt 14
17 #define SOCKOP_getsockopt 15
18 #define SOCKOP_sendmsg 16
19 #define SOCKOP_recvmsg 17
20 ……
但是sys_socketcall(int call , )函数中第一个call参数(即系统调用函数)都已SYS_打头和此处的socketcall.h中的SOCKOP_socket 不一样啊?但是在inlcude/linux/net.h中看到:
对比net.h和socketcall.h我们可以得出:二者中个函数调用的名称(标识)虽不同,但是有一点我们可以发现,就是他们的值想同:(即) SYS_SOCKET <=> 1<=> SOCKOP_socket ;
所以:
asmlinkage int sys_socketcall(int call, unsigned long *args)
4 {
5 int er;
6 switch(call)
7 {
8 case SYS_SOCKET:
9 er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
10 if(er)
11 return er;
12 return(sock_socket(get_fs_long(args+0),
13 get_fs_long(args+1),
14 get_fs_long(args+2)));
15 ……
16 case SYS_ACCEPT:
17 er=verify_area(VERIFY_READ, args, 3 * sizeof(long));
18 if(er)
19 return er;
20 return(sock_accept(get_fs_long(args+0),
21 (struct sockaddr *)get_fs_long(args+1),
22 (int *)get_fs_long(args+2)));
23 …
中的switch也就找到相应的case了。接下来就是sock_socket的工作了...这就是socket内陷内核的过程,交给内核后,内核会根据family输的的参数决定域做函数集用于socket结构中的ops字段赋值,如inet_proto_ops,unix_proto_ops。之后系统调用sock->ops->create生成一个下层sock(内核socket结构)结构并将其绑定在当前进程上。最后调用get_fd()分配一个文件描述符及其对应的FILE结构返回给应用层,另一部分会与文件描述符绑定并还回给应用层,该描述符就被赋值到listenfd了。其他函数我想大家也就一目了然了。。
(参考:Unix网络编程,Linux内核网络堆栈)
2012.10.13
16:45
--Y-uptoyking