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产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。

 

新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

 

查看系统调用的大体情况;

Socket与系统调用_第1张图片

 

 

 

访问系统调用

内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。

 

在进程上下文中,内核可以休眠并且可以被抢占。这两点都很重要。首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。休眠的能力会给内核编程带来极大便利。在进程上下文中能够被抢占,其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重人的。当然,这也是在对称多处理中必须同样关心的问题。

 

当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去。

 

设置断点进入内核

首先在MenuOS系统中运行hello文件;

Socket与系统调用_第2张图片

 

 

 

然后在gdb中设置断点,查看内核函数入口地址;

 

Socket与系统调用_第3张图片

 

 

 

分析内核源码

首先我们给出内核socket源码的结构体系;

Socket与系统调用_第4张图片

 

 

 

1、应用层——socket 函数

为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型。该函数只是作为一个简单的接口函数供用户调用,调用该函数后将进入内核栈进行系统调用sock_socket 函数。

#include 
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首部,目的端口号
    }

好吧,就看到这儿吧。。。看内核看到头秃。。。

你可能感兴趣的:(Socket与系统调用)