Socket与系统调用
系统调用
计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。
一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。在Unix世界,最流行的API是基于POSIX标准的。
操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。
中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。
一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call(),它与硬件体系有关,在entry.S中用汇编写。
前文已经提到了Linux下的系统调用是通过0x80实现的,但是我们知道操作系统会有多个系统调用(Linux下有319个系统调用),而对于同一个中断号是如何处理多个不同的系统调用的?最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:
系统调用的函数名称转换。
系统调用的参数传递。
首先看第一个问题。实际上,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。比如fork()的实现:
用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。
查看系统调用的大体情况;
访问系统调用
内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。
在进程上下文中,内核可以休眠并且可以被抢占。这两点都很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力会给内核编程带来极大便利。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重人的。当然,这也是在对称多处理中必须同样关心的问题。
当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。
设置断点进入内核
首先在MenuOS系统中运行hello文件;
然后在gdb中设置断点,查看内核函数入口地址;
分析内核源码
首先我们给出内核socket源码的结构体系;
1、应用层——socket 函数
为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型。该函数只是作为一个简单的接口函数供用户调用,调用该函数后将进入内核栈进行系统调用sock_socket 函数。
#include <sys/socket.h> int socket(int family, int type, int protocol);
2、BSD Socket 层——sock_socket 函数
从应用层进入该函数是通过一个共同的入口函数 sys_socket
首先是请求分配,调用具体的底层函数进行处理;
asmlinkage int sys_socketcall(int call, unsigned long *args) { int er; switch(call) { case SYS_SOCKET://socket函数 er=verify_area(VERIFY_READ, args, 3 * sizeof(long)); if(er) return er; return(sock_socket(get_fs_long(args+0), get_fs_long(args+1),//返回地址上的值 get_fs_long(args+2)));//调用sock_socket函数
然后来看sock_socket函数主体;
匹配应用程序调用socket()函数时指定的协议;
for (i = 0; i < NPROTO; ++i) { if (pops[i] == NULL) continue; if (pops[i]->family == family) //设置域 break; }
套接字类型检查;
if ((type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_SEQPACKET && type != SOCK_RAW && type != SOCK_PACKET) || protocol < 0) return(-EINVAL);
指定对应类型,协议,以及操作函数集
sock->type = type; sock->ops = ops;
分配下层sock结构,sock结构是比socket结构更底层的表示一个套接字的结构;
if ((i = sock->ops->create(sock, protocol)) < 0) //这里调用下层函数 create { sock_release(sock);//出错回滚销毁处理 return(i); }
分配一个文件描述符并在后面返回给应用层序作为以后的操作句柄
if ((fd = get_fd(SOCK_INODE(sock))) < 0) { sock_release(sock); return(-EINVAL); }
这时我们发现sock_socket 函数内部还调用了一个函数 sock_alloc(),该函数主要是分配一个 socket 套接字结构;
分配一个socket结构;
struct socket *sock_alloc(void) { struct inode * inode; struct socket * sock; inode = get_empty_inode();//分配一个inode对象 if (!inode) return NULL; //获得的inode结构的初始化 inode->i_mode = S_IFSOCK; inode->i_sock = 1; inode->i_uid = current->uid; inode->i_gid = current->gid; //可以看出socket结构体的实体空间,就已经存在了inode结构中的union类型中, //所以无需单独的开辟空间分配一个socket 结构 sock = &inode->u.socket_i;//这里把inode的union结构中的socket变量地址传给sock sock->state = SS_UNCONNECTED; sock->flags = 0; sock->ops = NULL; sock->data = NULL; sock->conn = NULL; sock->iconn = NULL; sock->next = NULL; sock->wait = &inode->i_wait; sock->inode = inode;//回绑 sock->fasync_list = NULL; sockets_in_use++;//系统当前使用的套接字数量加1 return sock; }
下面我们查看,INET Socket 层——inet_create 函数;该函数被上层sock_socket函数调用,用于创建一个socket套接字对应的sock结构并对其进行初始化;
分配一个sock结构,内存分配一个实体;
sk = (struct sock *) kmalloc(sizeof(*sk), GFP_KERNEL);
根据类型进行相关字段的赋值;
switch(sock->type) { case SOCK_STREAM: case SOCK_SEQPACKET: if (protocol && protocol != IPPROTO_TCP) { kfree_s((void *)sk, sizeof(*sk)); return(-EPROTONOSUPPORT); } protocol = IPPROTO_TCP;//tcp协议 sk->no_check = TCP_NO_CHECK; //这个prot变量表明了套接字使用的是何种协议 //然后使用的则是对应协议的操作函数 prot = &tcp_prot; break; case SOCK_DGRAM: if (protocol && protocol != IPPROTO_UDP) { kfree_s((void *)sk, sizeof(*sk)); return(-EPROTONOSUPPORT); } protocol = IPPROTO_UDP;//udp协议 sk->no_check = UDP_NO_CHECK;//不使用校验 prot=&udp_prot; break; case SOCK_RAW: if (!suser()) //超级用户才能处理 { kfree_s((void *)sk, sizeof(*sk)); return(-EPERM); } if (!protocol)// 原始套接字类型,这里表示端口号 { kfree_s((void *)sk, sizeof(*sk)); return(-EPROTONOSUPPORT); } prot = &raw_prot; sk->reuse = 1; sk->no_check = 0; /* * Doesn't matter no checksum is * performed anyway. */ sk->num = protocol;//本地端口号 break; case SOCK_PACKET: if (!suser()) { kfree_s((void *)sk, sizeof(*sk)); return(-EPERM); } if (!protocol) { kfree_s((void *)sk, sizeof(*sk)); return(-EPROTONOSUPPORT); } prot = &packet_prot; sk->reuse = 1; sk->no_check = 0; /* Doesn't matter no checksum is * performed anyway. */ sk->num = protocol; break; default://不符合以上任何类型,则返回 kfree_s((void *)sk, sizeof(*sk)); return(-ESOCKTNOSUPPORT); }
根据不同协议类型,调用对应init函数;
if (sk->prot->init) { err = sk->prot->init(sk);//调用相对应4层协议的初始化函数 if (err != 0) { destroy_sock(sk); return(err); } }
Bind()
下面我们再选择bind()函数进行分析;
sock_bind 函数主要就是将用户缓冲区的地址结构复制到内核缓冲区,然后转调用下一层的bind函数;
套接字参数有效性检查;
if (fd < 0 || fd >= NR_OPEN || current->files->fd[fd] == NULL) return(-EBADF);
获取fd对应的socket结构;
if (!(sock = sockfd_lookup(fd, NULL))) return(-ENOTSOCK);
将地址从用户缓冲区复制到内核缓冲区,umyaddr->address;
if((err=move_addr_to_kernel(umyaddr,addrlen,address))<0) return err;
转调用bind指向的函数,下层函数(inet_bind);
if ((i = sock->ops->bind(sock, (struct sockaddr *)address, addrlen)) < 0) { return(i); }
在进行地址绑定时,该套接字应该处于关闭状态;
if (sk->state != TCP_CLOSE) return(-EIO); //地址长度字段校验 if(addr_len<sizeof(struct sockaddr_in)) return -EINVAL;
非原始套接字类型,绑定前,没有端口号,则绑定端口号;
if(sock->type != SOCK_RAW) { if (sk->num != 0)//从inet_create函数可以看出,非原始套接字类型,端口号是初始化为0的 return(-EINVAL); snum = ntohs(addr->sin_port);//将地址结构中的端口号转为主机字节顺序 //如果端口号为0,则自动分配一个 if (snum == 0) { snum = get_new_socknum(sk->prot, 0);//得到一个新的端口号 } //端口号有效性检验,1024以上,超级用户权限 if (snum < PROT_SOCK && !suser()) return(-EACCES); }
检查地址是否是一个本地接口地址
chk_addr_ret = ip_chk_addr(addr->sin_addr.s_addr);
如果指定的地址不是本地地址,并且也不是一个多播地址,则错误返回
if (addr->sin_addr.s_addr != 0 && chk_addr_ret != IS_MYADDR && chk_addr_ret != IS_MULTICAST) return(-EADDRNOTAVAIL); /* Source address MUST be ours! */
如果没有指定地址,则系统自动分配一个本地地址
if (chk_addr_ret || addr->sin_addr.s_addr == 0) sk->saddr = addr->sin_addr.s_addr;//本地地址绑定 if(sock->type != SOCK_RAW) { /* Make sure we are allowed to bind here. */ cli(); }
检查检查有无冲突的端口号以及本地地址,有冲突,但不允许地址复用,退出;或者定位到了哈希表sock_array指定索引的链表的末端;
for(sk2 = sk->prot->sock_array[snum & (SOCK_ARRAY_SIZE -1)]; sk2 != NULL; sk2 = sk2->next) { /* should be below! */ if (sk2->num != snum) //没有重复,继续搜索下一个 continue;//除非有重复,否则后面的代码将不会被执行 if (!sk->reuse)//端口号重复,如果没有设置地址复用标志,退出 { sti(); return(-EADDRINUSE); } if (sk2->num != snum) continue; /* more than one */ if (sk2->saddr != sk->saddr) //地址和端口一个意思 continue; /* socket per slot ! -FB */ //如果状态是LISTEN表明该套接字是一个服务端,服务端不可使用地址复用选项 if (!sk2->reuse || sk2->state==TCP_LISTEN) { sti(); return(-EADDRINUSE); } } sti(); remove_sock(sk);//将sk sock结构从其之前的表中删除,inet_create中 put_sock,这里remove_sock put_sock(snum, sk);//然后根据新分配的端口号插入到新的表中。可以得知系统在维护许多这样的表 sk->dummy_th.source = ntohs(sk->num);//tcp首部,源端口号绑定 sk->daddr = 0;//sock结构所代表套接字的远端地址 sk->dummy_th.dest = 0;//tcp首部,目的端口号 }
好吧,就看到这儿吧。。。看内核看到头秃。。。