• Linux高性能服务器编程 学习笔记 第十六章 服务器调制、调试和测试


    Linux平台的一个优秀特性是内核微调,即我们可以通过修改文件的方式来调整内核参数。

    服务器开发过程中,可能会碰到意想不到的错误,一种调试方法是用tcpdump抓包,但这种方法主要用于分析程序的输入和输出,对于服务器的逻辑错误,更方便的调试方法是使用gdb调试器。

    系统分配给应用进程的文件描述符数量是有限制的,所以我们必须关闭那些已经不再使用的文件描述符,以释放它们占用的资源,比如作为守护进程运行的服务器就应该总是关闭标准输入、标准输出、标准错误这3个文件描述符。

    Linux对应用进程能打开的最大文件描述符数量有两个层次的限制:用户级限制和系统级限制。用户级限制指目标用户运行的所有进程总共能打开的文件描述符数;系统级限制指所有用户总共能打开的文件描述符数。

    以下命令可查看用户级文件描述符限制:

    ulimit -n
    
    • 1

    我们可通过一下方式将用户级文件描述符限制设为2048:

    ulimit -SHn 2048
    
    • 1

    但这种设置是临时的,只在当前session中有效,为永久修改用户级文件描述符数限制,可在/etc/security/limits.conf文件中加入以下两项:

    * hard nofile 2048
    * soft nofile 2048
    
    • 1
    • 2

    第一行是硬限制,第二行是软限制。*是通配符,表示所有用户。

    如果要修改系统级文件描述符数限制,可用以下命令:

    sysctl -w fs.file-max=2048
    
    • 1

    但该命令也是临时更改系统限制,要永久更改系统级文件描述符数限制,需要在/etc/sysctl.conf文件中添加以下项:

    fs.file-max=2048
    
    • 1

    然后执行sysctl -p命令,该Linux系统上的命令用于重新加载系统内核参数设置。

    几乎所有内核模块,包括内核核心模块和驱动程序,都在/proc/sys目录下提供了某些配置文件以供用户调整模块的属性和行为。通常一个配置文件对应一个内核参数,文件名就是参数的名字,文件内容就是参数的值。我们可通过命令sysctl -a查看所有这些内核参数,我们只讨论其中与网络编程关系较为紧密的内核参数。

    /proc/sys/fd目录下的内核参数都与文件系统相关,对服务器程序来说,有以下重要参数:
    1./proc/sys/fd/file-max:系统级文件描述符数限制,修改它是临时性修改,与以上所述临时修改系统级文件描述符数的限制效果相同。一般修改完该文件后,需要把/proc/sys/fd/inode-max设置为新/proc/sys/fd/file-man值的3~4倍,否则可能导致i节点数不够用。

    2./proc/sys/fd/epoll/max-user_watches:一个用户能够往epoll内核事件表中注册的事件总量。它是某用户打开的所有epoll实例总共能监听的事件数目,而不是单个epoll实例能监听的事件数目。往epoll内核事件表中注册一个事件,在32位系统上大概消耗90字节的内核空间,在64位系统上则消耗160字节的内核空间。这个内核参数限制了epoll使用的内核内存总量。

    内核中网络模块的相关参数都位于/proc/sys/net目录下,其中和TCP/IP协议相关的参数主要位于以下3个子目录中:core、ipv4、ipv6,以下是和服务器性能相关的部分参数:
    1./proc/sys/net/core/somaxconn:指定listen函数监听队列里,能建立完整连接从而进入ESTABLISHED状态的socket的最大数目。

    2./proc/sys/net/ipv4/tcp_max_syn_backlog:指定listen函数监听队列里,能够转移至ESTABLISHED或SYN_RCVD状态的socket的最大数目。

    3./proc/sys/net/ipv4/tcp_wmem:它包含3个值,分别指定一个socket的TCP写缓冲区的最小值、默认值、最大值。

    4./proc/sys/net/ipv4/tcp_rmem:它包含3个值,分别指定一个socket的TCP读缓冲区的最小值、默认值、最大值。

    5./proc/sys/net/ipv4/tcp_syncookies:指定是否打开TCP同步标签(syncookie),同步标签通过启动cookie来防止一个监听socket因不停地重复接收来自同一个地址的连接请求(同步报文段),而导致listen函数监听队列溢出(所谓的SYN风暴)。

    除了通过直接修改文件的方式来修改这些系统参数外,我们也可使用sysctl命令来修改它们,这两种修改方式都是临时的,永久的修改方法是在/etc/sysctl.conf文件中加入相应网络参数及其数值,并执行sysctl -p使之生效。

    以下讨论如何使用gdb来调试多进程和多线程程序,我们假设读者懂得基本的gdb调试方法,如设置断点、查看变量等。

    如果一个进程通过fork系统调用创建了子进程,gdb会继续调试原来的进程,子进程则正常运行,以下方式可调试子进程:
    1.单独调试子进程。子进程本质上来说也是一个进程,因此我们可通过通用的gdb调试方法来调试它,例如,我们可先运行服务器,然后找到子进程的PID,再将其附加(attach)到gdb调试器上,具体操作如下:
    在这里插入图片描述
    在这里插入图片描述
    上图中,b命令表示设置断点,格式为b filename:linenumber;c命令的作用是继续程序的执行,直到遇到下一个断点或程序正常结束;bt命令的作用是打印当前程序的函数调用堆栈(backtrace),显示当前执行路径中各个函数的调用关系和调用帧信息。

    2.使用调试器选项follow-fork-mode。gdb调试器的选项follow-fork-mode允许我们选择程序在执行fork系统调用后是继续调试父进程还是调试子进程,其用法如下:
    在这里插入图片描述
    上图中,mode的可选值是parent和child,分别表示调试父进程和子进程,使用前面的例子,这次使用follow-fork-mode选项来调试子进程,具体过程如下:
    在这里插入图片描述
    在这里插入图片描述
    上图中,gdb ./cgisrv命令以gdb启动名为cgisrv的可执行文件,当我们设置完调试器选项follow-fork-mode和断点后,使用r命令启动被调试的程序。

    gdb有一组命令可辅助多线程程序的调试,以下是其中一些常用的命令:
    1.info threads:显示当前可调式的所有线程。gdb会为每个线程分配一个ID,我们可使用这个ID来操作对应的线程,ID前面有*的线程是当前被调试的线程。

    2.thread ID:调试目标ID指定的线程。

    3.set scheduler-locking [off|on|stop]:调试多线程程序时,默认除了被调试的线程在执行外,其他线程也在继续执行,但有时我们仅希望被调试的线程运行,这可通过该命令来实现。该命令设置scheduler-locking的值:off表示不锁定任何线程,即所有线程都可继续执行,这是默认值;on表示只有当前被调试的线程会继续执行;stop表示在单步执行的时候,只有当前线程会执行。

    以下过程独立调试每个线程:
    在这里插入图片描述
    在这里插入图片描述
    上图中,gdb的n命令的作用是单步执行程序,一次性执行一行源代码,然后将控制权交还给调试器,以便检查程序状态、变量值和执行路径。

    一个关于调试进程池和线程池程序的不错的方法是,将池中的进程或线程个数减少至1,以观察程序的逻辑是否正确,然后逐步增加进程或线程数量,以调试进程或线程的同步是否正确。

    压力测试程序有很多种实现方式,如IO复用方式、多线程(进程)并发编程方式,以及这些方式的结合使用,但单纯的IO复用方式的施压程度是最高的,因为线程和进程的调度本身也要占用一定CPU时间(作者此处认为单线程、单进程的压力测试程序施压程度最高,但单线程、单进程只能同时使用CPU的一个核心,而多线程或多进程情况下可以使用CPU的多个核心,为什么多核心同时施压比单核心施压程度高呢?),因此我们使用epoll来实现一个通用的服务器压力测试程序:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    // 每个客户连接不停向服务器发送这个请求
    static const char *request = "GET http://localhost/index.html HTTP/1.1\r\n"
        "Connection: keep-alive\r\n\r\nxxxxxxxxxxxx";
    
    int setnonblocking(int fd) {
        int old_option = fcntl(fd, F_GETFL);
        int new_option = old_option | O_NONBLOCK;
        fcntl(fd, F_SETFL, new_option);
        return old_option;
    }
    
    void addfd(int epoll_fd, int fd) {
        epoll_event event;
        event.data.fd = fd;
        event.events = EPOLLOUT | EPOLLET | EPOLLERR;
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);
    }
    
    // 向服务器写入len字节的数据
    bool write_nbytes(int sockfd, const char *buffer, int len) {
        int bytes_write = 0;
        printf("write out %d bytes to socket %d\n", len, sockfd);
        while (1) {
            bytes_write = send(sockfd, buffer, len, 0);
            if (bytes_write == -1) {
                return false;
            } else if (bytes_write == 0) {
                return false;
            }
            
            len -= bytes_write;
            buffer = buffer + bytes_write;
            if (len <= 0) {
                return true;
            }
        }
    }
    
    // 从服务器读取数据
    bool read_once(int sockfd, char *buffer, int len) {
        int bytes_read = 0;
        memset(buffer, '\0', len);
        bytes_read = recv(sockfd, buffer, len, 0);
        if (bytes_read == -1) {
            return false;
        } else if (bytes_read == 0) {
            return false;
        }
        printf("read in %d bytes from socket %d with content: %s\n", bytes_read, sockfd, buffer);
        
        return true;
    }
    
    // 向服务器发起num参数个TCP连接,我们可以通过改变num参数来调整测试压力
    void start_conn(int epoll_fd, int num, const char *ip, int port) {
        int ret = 0;
        struct sockaddr_in address;
        bzero(&address, sizeof(address));
        address.sin_family = AF_INET;
        inet_pton(AF_INET, ip, &address.sin_addr);
        address.sin_port = htons(port);
        
        for (int i = 0; i < num, ++i) {
            sleep(1);
            int sockfd = socket(PF_INET, SOCK_STREAM, 0);
            printf("create 1 sock\n");
            if (sockfd < 0) {
                continue;
            }
            
            if (connect(sockfd, (struct sockaddr *)&address, sizeof(address)) == 0) {
                printf("build connection %d\n", i);
                addfd(epoll_fd, sockfd);
            }
        }
    }
    
    void close_conn(int epoll_fd, int sockfd) {
        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sockfd, 0);
        close(sockfd);
    }
    
    int main(int argc, char *argv[]) {
        assert(argc == 4);
        int epoll_fd = epoll_create(100);
        start_conn(epoll_fd, atoi(argv[3]), argv[1], atoi(argv[2]));
        epoll_event events[10000];
        char buffer[2048];
        while (1) {
            // 第3个参数是第2个参数数组的大小
            // 第4个参数是等待的毫秒数
            int fds = epoll_wait(epoll_fd, events, 10000, 2000);
            for (int i = 0; i < fds; ++i) {
                int sockfd = events[i].data.fd;
                if (events[i].events & EPOLLIN) {
                    // 此处代码有问题,当读取失败后(对端关闭连接或读函数失败)
                    // 先关闭了连接,然后接着又监听被关闭连接的可写状态,此处关闭连接后,应该continue
                    if (!read_once(sockfd, buffer, 2048)) {
                        close_conn(epoll_fd, sockfd);
                    }
                    struct epoll_event event;
                    event.events = EPOLLOUT | EPOLLET | EPOLLERR;
                    event.data.fd = sockfd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, sockfd, &event);
                } else if (events[i].events & EPOLLOUT) {
                    // 此处也是同样的问题
                    if (!write_nbytes(sockfd, request, strlen(request))) {
                        close_conn(epoll_fd, sockfd);
                    }
                    struct epoll_event event;
                    event.events = EPOLLIN | EPOLLET | EPOLLERR;
                    event.data.fd = sockfd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, sockfd, &event);
                } else if (events[i].events & EPOLLERR) {
                    close_conn(epoll_fd, sockfd);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131

    使用以上压力测试程序(名为stress_test)来测试第十五章中用线程池实现的Web服务器,我们现在测试机器ernest-laptop上运行websrv,然后从Kongming20上执行stress_test,向websrv服务器发起1000个连接,具体操作如下:
    在这里插入图片描述
    如果websrv服务器程序足够稳定,那么websrv和stress_test这两个进程将一直运行下去,并不断交换数据。

  • 相关阅读:
    牛客网刷题 | BC117 逆序输出
    大数据随记 —— WordCount 案例
    宜搭能否实现多人打分
    QT C++ 基于TCP通信的网络聊天室
    LeetCode刷题(10)
    数字孪生|数字孪生装备-概念与内涵
    vue面试题
    中国SaaS行业等待“渡劫时刻”
    聊聊如何获取PreparedStatement的参数
    docker中DVWA的安装
  • 原文地址:https://blog.csdn.net/tus00000/article/details/133865517