结合源码学习 TCP/IP 协议栈(2)

  • 执行脚本

    在 level-ip 中,我们是重新封装了执行应用层命令的底层 socket 库,其中执行 curl 的命令为

    ./level-ip curl www.baidu.com 80
    

    看一下这个 level-ip 脚本

    #!/bin/sh
    
    set -eu
    
    prog="$1"
    shift
    
    LD_PRELOAD="$(dirname $0)/liblevelip.so" "$prog" "$@"
    

    用 LD_PRELOAD 加载了 liblevelip.so 库,而这个库又是由 liblevellip.c 编译而来,显然重写的 socket 执行逻辑也在这个文件中实现。

  • liblevellip 中的实现

    首先是 __libc_start_main 函数

    int __libc_start_main(int (*main) (int, char * *, char * *), int argc,
                          char * * ubp_av, void (*init) (void), void (*fini) (void),
                          void (*rtld_fini) (void), void (* stack_end))
    {
        __start_main = dlsym(RTLD_NEXT, "__libc_start_main");
    	// 将从其他动态库去加载 __libc_start_main 这个符号的时候,把函数句柄赋值给 __start_main 变量
        _sendto = dlsym(RTLD_NEXT, "sendto");
        _recvfrom = dlsym(RTLD_NEXT, "recvfrom");
        _poll = dlsym(RTLD_NEXT, "poll");
        _ppoll = dlsym(RTLD_NEXT, "ppoll");
        _pollchk = dlsym(RTLD_NEXT, "__poll_chk");
        _select = dlsym(RTLD_NEXT, "select");
        _fcntl = dlsym(RTLD_NEXT, "fcntl");
        _setsockopt = dlsym(RTLD_NEXT, "setsockopt");
        _getsockopt = dlsym(RTLD_NEXT, "getsockopt");
        _read = dlsym(RTLD_NEXT, "read");
        _write = dlsym(RTLD_NEXT, "write");
        _connect = dlsym(RTLD_NEXT, "connect");
        _socket = dlsym(RTLD_NEXT, "socket");
        _close = dlsym(RTLD_NEXT, "close");
        _getpeername = dlsym(RTLD_NEXT, "getpeername");
        _getsockname = dlsym(RTLD_NEXT, "getsockname");
    	// 从 glibc 中加载一部分 linux 原生的系统调用接口
        list_init(&lvlip_socks);	// 初始化链表结点
    
        return __start_main(main, argc, ubp_av, init, fini, rtld_fini, stack_end);
    }
    

    关于我们自己封装的 socket 函数,

    int socket(int domain, int type, int protocol)
    {
        if (!is_socket_supported(domain, type, protocol)) {
            return _socket(domain, type, protocol);
        }
    	// 如果不是 tcp 协议,则调用内核提供的网络服务
        struct lvlip_sock *sock;
        
        int lvlfd = init_socket("/tmp/lvlip.socket");
    	// 与 tcp 协议栈建立连接,初始化这个文件描述符用于通信
        sock = lvlip_alloc();	// 申请空间
        sock->lvlfd = lvlfd;	// 记录文件描述符
        list_add_tail(&sock->list, &lvlip_socks);	// 把这次连接加入 lvlip_socks 链表中
        lvlip_socks_count++;
        
        int pid = getpid();
        int msglen = sizeof(struct ipc_msg) + sizeof(struct ipc_socket);	// 用于发送详细的 socket 信息到 tcp 协议栈
    
        struct ipc_msg *msg = alloca(msglen);
        msg->type = IPC_SOCKET;
        msg->pid = pid;
    
        struct ipc_socket usersock = {
            .domain = domain,
            .type = type,
            .protocol = protocol
        };
        
        memcpy(msg->data, &usersock, sizeof(struct ipc_socket));
    	// 把 ipc_socket 中的内容填充到 ipc_msg 的 data 段中
        int sockfd = transmit_lvlip(sock->lvlfd, msg, msglen);
    	// 发送数据
        if (sockfd == -1) {
            /* Socket alloc failed */
            lvlip_free(sock);
            return -1;
        }
    
        sock->fd = sockfd;
    
        lvl_sock_dbg("Socket called", sock);
        
        return sockfd;	//
    }
    

    其中用到的各种结构:

    lvlip_sock (管理每个 socket 的信息):

    struct lvlip_sock {
        struct list_head list;		// 链表结点
        int lvlfd; 		// 与 tcp 协议通信的网络文件描述符
        int fd;		// 记录 tcp 协议栈的返回状态
    };
    

    ipc_msg:

    struct ipc_msg {
        uint16_t type;		// 记录这次 socket 信息的类型 
        pid_t pid;			// 这次网络请求进程的 pid
        uint8_t data[];		// 通信的内容
    } __attribute__((packed));
    

    ipc_socket:

    struct ipc_socket {		// 调用 socket 函数的 3 个参数
        int domain;
        int type;
        int protocol;
    } __attribute__((packed));
    

    connect 函数的实现,在 linux 中可以不进行 bind 就直接调用 connect 函数,会由系统自身打开端口并建立连接。

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
    {
        struct lvlip_sock *sock = lvlip_get_sock(sockfd);
    	// 得到传入的 sockfd,并在 list 中找到对应的 sock 结构体地址
        if (sock == NULL) {
            /* No lvl-ip IPC socket associated */
            return _connect(sockfd, addr, addrlen);
        }
        // 如果没找到就调用底层的 _connect 进行连接
    
        lvl_sock_dbg("Connect called", sock);
        
        // 下面的部分和 socket 的实现相似了
        int msglen = sizeof(struct ipc_msg) + sizeof(struct ipc_connect);
        int pid = getpid();
        
        struct ipc_msg *msg = alloca(msglen);
        msg->type = IPC_CONNECT;
        msg->pid = pid;
    
        struct ipc_connect payload = {
            .sockfd = sockfd,
            .addr = *addr,
            .addrlen = addrlen
        };
    
        memcpy(msg->data, &payload, sizeof(struct ipc_connect));
    
        return transmit_lvlip(sock->lvlfd, msg, msglen);
    }
    

    其实这时翻一下 liblevellip 中其他函数的实现,和 socket 的逻辑相似,都是经过:

    查找 fd -> 填充数据 -> 用 transmit_lvlip 发送数据
    

    而对于 sock == NULL 的情况,都是调用了 src/socket.c 中的对应函数做了更底层的实现。

    看一下负责发送的 transmit_lvlip 函数,其实也是调用的底层的 _read 和 _write 函数进行数据收发,然后根据响应的内容返回。

    static int transmit_lvlip(int lvlfd, struct ipc_msg *msg, int msglen)
    {
        char *buf[RCBUF_LEN];
    
        // Send mocked syscall to lvl-ip
        if (_write(lvlfd, (char *)msg, msglen) == -1) {
            perror("Error on writing IPC");
        }
    
        // Read return value from lvl-ip
        if (_read(lvlfd, buf, RCBUF_LEN) == -1) {
            perror("Could not read IPC response");
        }
        
        struct ipc_msg *response = (struct ipc_msg *) buf;
    
        if (response->type != msg->type || response->pid != msg->pid) {
            print_err("ERR: IPC msg response expected type %d, pid %d\n"
                      "                      actual type %d, pid %d\n",
                   msg->type, msg->pid, response->type, response->pid);
            return -1;
        }
    
        struct ipc_err *err = (struct ipc_err *) response->data;
    
        if (err->rc == -1) errno = err->err;
    
        return err->rc;
    }
    
  • main 函数的流程

    int main(int argc, char** argv)
    {
        parse_cli(argc, argv);	// 解析接收到的命令
        
        init_signals();		// 初始化信号量
        init_stack();		// 初始化协议栈
        init_security();	// 线程安全初始化
        
        run_threads();		// 启动协议栈
        wait_for_threads();	// 结束进程
    
        free_stack();	// 释放资源
    }
    

    重点看一下 run_thread 中启动的这几个进程,

    static void run_threads()
    {
        create_thread(THREAD_CORE, netdev_rx_loop);
        create_thread(THREAD_TIMERS, timers_start);
        create_thread(THREAD_IPC, start_ipc_listener);
        create_thread(THREAD_SIGNAL, stop_stack_handler);
    }
    

    其中的 netdev_rx_loop 函数就是调用了之前提到过的 netdev_receive,等待接收数据包,并根据类型分配给对应的处理逻辑(arp/ip)。

    timers_start 和 stop_stack_handler 函数主要是和多线程相关的操作,不是重点,略。

    重点看一下 start_ipc_listener,socket 的主要操作都在这里面实现(src/ipc.c)。

    void *start_ipc_listener()
    {
        int fd, rc, datasock;
        struct sockaddr_un un;
        char *sockname = "/tmp/lvlip.socket";	// 指定 tcp 本地通信的路径文件
    
        unlink(sockname);
        
        if (strnlen(sockname, sizeof(un.sun_path)) == sizeof(un.sun_path)) {
            // Path is too long
            print_err("Path for UNIX socket is too long\n");
            exit(-1);
        }
        
        if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
            perror("IPC listener UNIX socket");
            exit(EXIT_FAILURE);
        }
        // 调用 socket 进行 tcp 本地通信    
    
        memset(&un, 0, sizeof(struct sockaddr_un));
        un.sun_family = AF_UNIX;
        strncpy(un.sun_path, sockname, sizeof(un.sun_path) - 1);
    	
        rc = bind(fd, (const struct sockaddr *) &un, sizeof(struct sockaddr_un));
      	// 绑定本地通信路径
        
        if (rc == -1) {
            perror("IPC bind");
            exit(EXIT_FAILURE);
        }
    
        rc = listen(fd, 20);	// 监听端口,等待 liblivelip 库发起连接
    
        if (rc == -1) {
            perror("IPC listen");
            exit(EXIT_FAILURE);
        }
    
        if (chmod(sockname, S_IRUSR | S_IWUSR | S_IXUSR |
                  S_IRGRP | S_IWGRP | S_IXGRP |
                  S_IROTH | S_IWOTH | S_IXOTH) == -1) {
            perror("Chmod on lvl-ip IPC UNIX socket failed");
            exit(EXIT_FAILURE);
        }
    
        // 收到连接,调用 accept 来接收信息
        for (;;) {
            datasock = accept(fd, NULL, NULL);
            if (datasock == -1) {
                perror("IPC accept");
                exit(EXIT_FAILURE);
            }
    
            struct ipc_thread *th = ipc_alloc_thread(datasock);
    
            // 每监听到一个新的连接,就创建一个 socket_ipc_open 来进行数据的具体收发
            if (pthread_create(&th->id, NULL, &socket_ipc_open, &th->sock) != 0) {
                print_err("Error on socket thread creation\n");
                exit(1);
            };
        }
    
        close(fd);
    
        unlink(sockname);
    
        return NULL;
    }
    

    进一步的处理逻辑要跟进 socket_ipc_open 函数

    void *socket_ipc_open(void *args) {
        int blen = IPC_BUFLEN;
        char buf[blen];
        int sockfd = *(int *)args;
        int rc = -1;
    
        while ((rc = read(sockfd, buf, blen)) > 0) {
            // 读取 socket 状态
            rc = demux_ipc_socket_call(sockfd, buf, blen);
    		// 拿到具体指令的回调信息,执行 socket 中标记的应当执行的步骤
            if (rc == -1) {
                print_err("Error on demuxing IPC socket call\n");
                close(sockfd);
                return NULL;
            };
        }
    	// 释放资源
        ipc_free_thread(sockfd);
    
        if (rc == -1) {
            perror("socket ipc read");
        }
        
        return NULL;
    }
    

    socket 的状态选项,在 demux_ipc_socket_call 中

    static int demux_ipc_socket_call(int sockfd, char *cmdbuf, int blen)
    {
        struct ipc_msg *msg = (struct ipc_msg *)cmdbuf;
    
        switch (msg->type) {
        case IPC_SOCKET:
            return ipc_socket(sockfd, msg);
            break;
        case IPC_CONNECT:
            return ipc_connect(sockfd, msg);
            break;
        case IPC_WRITE:
            return ipc_write(sockfd, msg);
            break;
        case IPC_READ:
            return ipc_read(sockfd, msg);
            break;
        case IPC_CLOSE:
            return ipc_close(sockfd, msg);
            break;
        case IPC_POLL:
            return ipc_poll(sockfd, msg);
            break;
        case IPC_FCNTL:
            return ipc_fcntl(sockfd, msg);
            break;
        case IPC_GETSOCKOPT:
            return ipc_getsockopt(sockfd, msg);
        case IPC_GETPEERNAME:
            return ipc_getpeername(sockfd, msg);
        case IPC_GETSOCKNAME:
            return ipc_getsockname(sockfd, msg);
        default:
            print_err("No such IPC type %d\n", msg->type);
            break;
        };
        
        return 0;
    }
    

    在上层调用 socket 的时候,传入的消息类型是 IPC_SOCKET,跟进 ipc_socket 看一下实现逻辑:

    static int ipc_socket(int sockfd, struct ipc_msg *msg)
    {
        struct ipc_socket *sock = (struct ipc_socket *)msg->data;
        pid_t pid = msg->pid;
        int rc = -1;
    
        rc = _socket(pid, sock->domain, sock->type, sock->protocol);	// 最终还是调用了底层的 _socket 函数
    
        return ipc_write_rc(sockfd, pid, IPC_SOCKET, rc);
        // 把 tcp 协议栈的处理结果返回给 liblevelip
    }
    
  • 总结

    无论是项目中持续运行的 tcp 协议栈,还是自己更换动态库的应用程序,用到的 socket 都是将底层接口层层封装,在每一层补充和添加对应的信息,大概下一篇就要看底层的 _socket 系列函数的实现了。

posted @ 2022-03-24 13:35  moon_flower  阅读(122)  评论(0编辑  收藏  举报