结合源码学习 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 系列函数的实现了。