一、Socket API编程接口之上可以编写基于不同网络协议的应用程序

在之前的博客中已经详细分析了Socket API,并用JAVA实现了简单的基于TCP/IP协议的hello/hi程序,这里只做简要的复习。

创建socket:

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

socket()打开一个网络通讯端口,如果成功的话,返回一个socket文件描述符(有一种文件类型是socket文件),如果socket()调用失败则返回-1。

应用程序可以像读写文件一样用read/write在网络上收发数据。

命名socket:

int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。讲一个socket文件描述符与socket地址绑定起来的动作叫做socket命名。

服务器程序所监听的网络地址和端口号通常是固定不变的,因此服务器程序中通常要命名socket,这样客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接。需要调用bind绑定一个socket地址(socket地址中包含网络地址(IP)和端口号(port));

客户端则不用命名socket,而是通常采用匿名方式,即使用操作系统自动分配的socket地址。

监听socket:

int listen(int sockfd, int backlog);

socket被命名之后,还不能马上接受客户端连接,我们需要使用listen函数来创建一个监听队列以存放待处理的客户连接。

listen系统调用使sockfd处于监听状态。

listen()成功返回0,失败返回-1。

接受连接accept:

int accept(int sockfd, struct sockaddr *addr, socklen_t *len);

三次握手完成后,服务器调用accept()从监听队列中接受一个连接。

如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待,直到有客户端连接上来。

accept调用成功返回一个新的socket文件描述符,该socket文件描述符唯一地标识了被接受的这个连接,服务器可通过读写这个新的socket文件描述符来与被接受连接的对应的客户端通信。

发起连接connect:

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

客户端需要调用connect()连接服务器。

connect和bind的参数形式一致,区别在于bind的参数是自己的地址,connect的参数是服务器的地址,即addr是服务器的socket地址,addrlen是服务器的地址的大小。

成功返回0,一旦成功,sockfd就唯一标识这个连接,客户端就可以通过sockfd来与服务器通信。connect()调用失败返回-1,并设置以下两个常见的errno。

关闭连接close/shutdown:

int close(int fd);
int shutdown(int sockfd,int howto);

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过调用关闭普通文件描述符的close()来完成。

close()系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork调用默认将使父进程打开的socket的引用计数加1,因此必须在父子进程都对该socket文件描述符进行close()才能够将连接关闭。

当无论如何都要立即关闭连接,而不是将引用计数减1,就可以使用shutdown系统调用,它是专门为网络编程设计的。

shutdown能够分别关闭sockfd上的读或写,或者同时关闭;而close只能将sockfd上的读和写同时关闭。

二、Socket接口在用户态通过系统调用机制进入内核

用户态、内核态和中断:

内核态:一般现代CPU有几种指令执行级别。在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态

用户态:在相应的低级别执行状态下,代码的掌控范围有限,只能在对应级别允许的范围内活动。如intel x86 CPU有四种不同的执行级别0-3,Linux只使用0级表示内核态,3级表示用户态。权限级别的划分使系统更稳定。

中断:是从用户态进入内核态的主要方式。系统调用只是一种特殊的中断。从用户态切换到内核态时:必须保存用户态的寄存器上下文,同时将内核态的寄存器相应的值放入当前CPU。

系统调用:

linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用,系统调用是用户态进入内核态的唯一入口。

API与系统调用:

Libc库定义的一些API引用了封装例程(wrapper routine,发布系统调用)使得程序员不需要通过汇编指令来触发系统调用:一般每个系统调用对应一个封装例程;库再用这些封装例程定义出给用户的API。

三、内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析

系统调用执行的流程:

应用程序代码调用系统调用,该函数是一个包装系统调用的库函数;

库函数负责准备向内核传递的参数,并触发软中断以切换到内核;

CPU被软中断打断后,执行中断处理函数,即系统调用处理函数;

系统调用处理函数调用系统调用服务例程,真正开始处理该系统调用;

socket系统调用实例:

在之前搭建的menuOS中对bind()函数进行系统调用跟踪。

先在终端启动menuOS,再重新打开一个终端进入gdb调试:

为bind()函数设置断点:

gdb
file linux-5.0.1/vmlinux
target remote:1234
break __sys_bind
break __sys_listen

设置好断点后继续运行,在menuOS中依次输入replyhi和hello,观察gdb中显示调用的函数为

__sys_bind(fd=3,backlog=1024) at net/socket.c, line 1469

打开socket.c文件,找到对应函数:

其中核心的处理语句为sock->ops->bind,此操作实际是调用了inet_stream_ops.bind,查看inet_stream_ops.bind结构:

const struct proto_ops inet_stream_ops = {
         .family                  = PF_INET,
         .owner                  = THIS_MODULE,
         .release      = inet_release,
         .bind                      = inet_bind,
         .connect     = inet_stream_connect,
         .socketpair          = sock_no_socketpair,
         .accept                 = inet_accept,
         .getname             = inet_getname,
         .poll              = tcp_poll,
         .ioctl                      = inet_ioctl,
         .listen                   = inet_listen,
         .shutdown           = inet_shutdown,
         .setsockopt         = sock_common_setsockopt,
         .getsockopt         = sock_common_getsockopt,
         .sendmsg             = inet_sendmsg,
         .recvmsg    = inet_recvmsg,
         .mmap                  = sock_no_mmap,
         .sendpage           = inet_sendpage,
         .splice_read        = tcp_splice_read,
#ifdef CONFIG_COMPAT
         .compat_setsockopt = compat_sock_common_setsockopt,
         .compat_getsockopt = compat_sock_common_getsockopt,
         .compat_ioctl     = inet_compat_ioctl,
#endif

其中bind调用了inet_bind()函数,bind系统调用通过套接口层Inet_bind(),然后便会调用传输接口层的函数,TCP中的传输层接口函数为inet_csk_get_port函数,该函数主要实现bind的作用,如果用户系统调用使用的端口号为0,系统会自动选择一个可用的端口号,这里选择可用端口号思路是:先在绑定表中选择可用的端口号,如果在绑定表中没有可用的端口号,再选择空闲的端口号。

/* 地址绑定 */
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
    struct sock *sk = sock->sk;
    struct inet_sock *inet = inet_sk(sk);
    struct net *net = sock_net(sk);
    unsigned short snum;
    int chk_addr_ret;
    u32 tb_id = RT_TABLE_LOCAL;
    int err;

    /* If the socket has its own bind function then use it. (RAW) */
    /*
        如果传输控制块有自己的bind操作则调用,
        目前只有raw实现了自己的bind
    */
    if (sk->sk_prot->bind) {
        err = sk->sk_prot->bind(sk, uaddr, addr_len);
        goto out;
    }

    err = -EINVAL;
    /* 地址长度错误 */
    if (addr_len < sizeof(struct sockaddr_in))
        goto out;

    /* 如果不是AF_INET协议族 */
    if (addr->sin_family != AF_INET) {
        /* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
         * only if s_addr is INADDR_ANY.
         */
        err = -EAFNOSUPPORT;

        /* 接受AF_UNSPEC && s_addr=htonl(INADDR_ANY)的情况 */
        if (addr->sin_family != AF_UNSPEC ||
            addr->sin_addr.s_addr != htonl(INADDR_ANY))
            goto out;
    }

    tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
    chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);

    /* Not specified by any standard per-se, however it breaks too
     * many applications when removed.  It is unfortunate since
     * allowing applications to make a non-local bind solves
     * several problems with systems using dynamic addressing.
     * (ie. your servers still start up even if your ISDN link
     *  is temporarily down)
     */
    err = -EADDRNOTAVAIL;

    /* 合法性检查 */
    if (!net->ipv4.sysctl_ip_nonlocal_bind &&
        !(inet->freebind || inet->transparent) &&
        addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
        chk_addr_ret != RTN_LOCAL &&
        chk_addr_ret != RTN_MULTICAST &&
        chk_addr_ret != RTN_BROADCAST)
        goto out;

    /* 源端口 */
    snum = ntohs(addr->sin_port);
    err = -EACCES;

    /* 绑定特权端口的权限检查 */
    if (snum && snum < inet_prot_sock(net) &&
        !ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
        goto out;

    /*      We keep a pair of addresses. rcv_saddr is the one
     *      used by hash lookups, and saddr is used for transmit.
     *
     *      In the BSD API these are the same except where it
     *      would be illegal to use them (multicast/broadcast) in
     *      which case the sending device address is used.
     */
    lock_sock(sk);

    /* Check these errors (active socket, double bind). */
    err = -EINVAL;

    /* 传输控制块的状态不是CLOSE || 存在本地端口 */
    if (sk->sk_state != TCP_CLOSE || inet->inet_num)
        goto out_release_sock;

    /* 设置源地址rcv_addr用作hash查找,saddr用作传输 */
    inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;

    /* 组播或者广播,使用设备地址 */
    if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
        inet->inet_saddr = 0;  /* Use device */

    /* Make sure we are allowed to bind here. */

    /*
        端口不为0,或者端口为0允许绑定
        则使用协议的具体获取端口函数绑定端口
    */
    if ((snum || !inet->bind_address_no_port) &&
        sk->sk_prot->get_port(sk, snum)) {

        /* 绑定失败 */
        inet->inet_saddr = inet->inet_rcv_saddr = 0;

        /* 端口在使用中 */
        err = -EADDRINUSE;
        goto out_release_sock;
    }

    /* 传输控制块已经绑定本地地址或端口标志 */
    if (inet->inet_rcv_saddr)
        sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
    if (snum)
        sk->sk_userlocks |= SOCK_BINDPORT_LOCK;

    /* 设置源端口 */
    inet->inet_sport = htons(inet->inet_num);

    /* 设置目的地址和端口默认值 */
    inet->inet_daddr = 0;
    inet->inet_dport = 0;

    /* 设置路由默认值 */
    sk_dst_reset(sk);
    err = 0;
out_release_sock:
    release_sock(sk);
out:
    return err;
}
EXPORT_SYMBOL(inet_bind);

在raw.c文件中的proto架构体的定义:

struct proto raw_prot = {
         .name                   = "RAW",
         .owner                  = THIS_MODULE,
         .close                    = raw_close,
         .destroy      = raw_destroy,
         .connect     = ip4_datagram_connect,
         .disconnect         = udp_disconnect,
         .ioctl                      = raw_ioctl,
         .init              = raw_init,
         .setsockopt         = raw_setsockopt,
         .getsockopt         = raw_getsockopt,
         .sendmsg             = raw_sendmsg,
         .recvmsg    = raw_recvmsg,
         .bind                      = raw_bind,
         .backlog_rcv       = raw_rcv_skb,
         .release_cb         = ip4_datagram_release_cb,
         .hash                     = raw_hash_sk,
         .unhash                = raw_unhash_sk,
         .obj_size     = sizeof(struct raw_sock),
         .h.raw_hash       = &raw_v4_hashinfo,
#ifdef CONFIG_COMPAT
         .compat_setsockopt = compat_raw_setsockopt,
         .compat_getsockopt = compat_raw_getsockopt,
         .compat_ioctl     = compat_raw_ioctl,
#endif
};
12-13 19:48