• C语言网络编程基础(linux)


    文件描述符与套接字

    linux操作系统下,有万物皆文件的概念,当一个进程想要打开/创建一个文件时,内核会给进程返回一个文件描述符,文件描述符是一个非负数,常用int类型表示,起到索引的作用,是为了高效管理进程打开/创建的文件的,指向的是被打开的文件。所有I/O的系统操作也都是通过文件描述符来的;每一个进程都有一个文件描述符表,里面记录的就是进程打开/创建文件的记录

    套接字是一种特殊的文件描述符,用于进程和进程之间的网络通信,常用在网络编程中

    进程和进程之间通信主要有六种方式,分别是:
    1.管道
    2.消息队列
    3.共享内存
    4.信号
    5.信号量
    6.套接字.

    套接字便是其中的一种.

    网络编程的基本流程

    在这里插入图片描述
    这个流程很经典,就不过多赘述了.

    基础的函数和结构体(持续更新)

    函数太多了,这里只记录一些常用的函数

    socket函数

    #include 
    
    int socket(int domain, int type, int protocol);
    
    • 1
    • 2
    • 3

    其中
    domain表示指定套接字的地址族或协议族。常见的值包括:

    AF_INET:用于IPv4 地址族。
    AF_INET6:用于IPv6 地址族。
    AF_UNIX 或 AF_LOCAL:用于本地(Unix 域)套接字通信。
    
    • 1
    • 2
    • 3

    type表示指定套接字的类型,常见的值包括:

    SOCK_STREAM:用于基于流的 TCP 套接字。
    SOCK_DGRAM:用于基于数据报的 UDP 套接字。
    SOCK_RAW:用于原始套接字,允许更底层的数据包处理。
    
    • 1
    • 2
    • 3

    protocol 参数通常为 0,表示选择默认的协议。在大多数情况下,操作系统会自动选择正确的协议,例如,对于 IPv4 TCP 套接字,它会选择 TCP 协议。

    返回值:socket函数的返回值是一个文件描述符(fd),经常作为网络编程中其他函数的参数.

    常见的使用方式

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    sockaddr和sockaddr_in结构体

    sockaddr

    #include 
    struct sockaddr {  
         sa_family_t sin_family;//地址族
        char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
       }; 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    sockaddr已经被sockaddr_in取代了,这里就不详细说了。

    sockaddr_in

    #include或#include 
    
    struct sockaddr_in {
        short int sin_family;      // 地址族(Address Family),通常为 AF_INET
        unsigned short int sin_port;  // 端口号(Port Number)
        struct in_addr sin_addr;     // IPv4 地址(32 位的 IPv4 地址)
        unsigned char sin_zero[8];   // 不使用,填充字节
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    sockaddr_in 是用于表示 IPv4 地址的 C 语言结构体,通常在网络编程中与套接字套接字相关的函数一起使用

    常见的使用方式:

    struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;//绑定地址族,使用ipv4
        addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1 //绑定地址
        addr.sin_port = htons(8000); //绑定端口
    
    • 1
    • 2
    • 3
    • 4
    • 5

    bind函数

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

    1.sockfd参数表示要进行绑定的套接字文件描述符(是socket函数的返回值)

    2.sockaddr 结构体是刚才上述所说的结构体,但是sockaddr不如sockaddr_in好用,所以一般情况下是定义一个sockaddr_in结构体,然后使用强制转换成sockaddr类型

    3.addrlen参数表示结构体的长度

    常用的使用方式:

     struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(8080);  // 端口号 8080
        server_addr.sin_addr.s_addr = INADDR_ANY;  // 任意地址
        memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));
        
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
            perror("Bind failed");
            exit(1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    listen函数

    listen函数作用:让套接字变成可以被动连接的状态,等待客户端的连接

    int listen(int sockfd, int backlog);
    
    • 1

    sockfd参数表示文件描述符

    backlog参数表示等待连接队列的最大长度,即在调用 accept 函数之前可以排队等待的最大连接数。通常,这个值为一个正整数,决定了同时等待的连接数量。

    常用的使用方法:

    int backlog = 5; // 最大等待连接数
    if (listen(sockfd, backlog) == -1) {
            perror("Listen failed");
            exit(1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    accept函数

    accept 函数用于接受传入的连接请求,通常在服务器端用于接受客户端的连接

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    • 1

    1.sockfd是文件描述符
    2.addr是 sockaddr结构体,用于接收客户端的地址,端口等信息,所以跟Bind函数调用时的sockaddr要区分开来

    3.addrlen是结构体的大小

    常见的使用方式:

     struct sockaddr_in new_addr;
     int new_sock;
     
     addr_size = sizeof(new_addr);
     new_sock = accept(sockfd, (struct sockaddr*)&new_addr, &addr_size);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    返回值:在成功接受连接请求时返回一个新的套接字,该套接字用于与客户端进行通信。这个新套接字是已连接套接字,它是服务器与客户端之间的通信通道。

    这里要重点强调一下,我们后续进行客户端和服务端之间的通信时,使用的是accept函数返回的新套接字,而之前用socket函数创建的旧套接字仍然在监听新的连接请求(用于接收连接请求,而不是直接用来通信)

    recv函数

    recv 函数用于从已连接套接字(或者数据报套接字)接收数据
    注意是已连接的套接字

    int recv(int sockfd, void *buf, size_t len, int flags);
    
    • 1

    1.sockfd是文件描述符
    2.buf是接收数据的缓冲区指针
    3.len是缓冲区的大小
    4.flags通常设置为0

    返回值是recv函数读到的字节数,如果返回值为 -1,表示读取失败,失败的原因会存储在errno里面
    recv函数的返回值总结

    常见的使用方式:

    int bytes_read=recv(sockfd,buffer,sizeof(buffer),0);
    
    • 1

    recv函数是一个阻塞函数,如果在读取时,发现并没有数据可以读,就会被阻塞住,如果不想被阻塞住,可以用fcntl函数将文件描述符设置为非阻塞模式,具体操作请看fcntl函数.

    recv 和 read 函数在某些方面类似,因为它们都用于从文件描述符中读取数据。然而,它们有一些区别:

    来源:
    recv 是套接字库函数,用于在网络编程中接收数据。它可以用于套接字(sockets)等网络通信相关的操作。
    read 是标准C I/O 函数,通常用于文件描述符,但也可以用于套接字等。它更一般化,可用于读取任何可读的文件描述符。

    参数:
    recv 在最后一个参数中可以指定额外的选项(flags),允许对接收操作进行控制。
    read 没有额外的选项参数,它只接受文件描述符、缓冲区和长度。

    错误处理:
    recv 返回的错误值可能包含更多关于套接字通信的信息,如连接已断开等。因此,错误代码可能更详细。
    read 的错误码可能相对简单,不会提供关于底层通信的额外信息,但它可用于读取多种文件类型。

    用法:
    recv 主要用于网络编程,特别是在套接字通信中,用于接收数据。
    read 主要用于文件和通用文件描述符的读取,可用于从文件、管道、套接字等读取数据。

    writev函数

    writev 函数用于将多个分散的数据写入文件描述符(通常是文件或套接字)
    也被称为集中写,与write函数的最大区别就是writev函数可以一次性写出多个缓冲区,而write函数一次性只能写出一个缓冲区

    #include 
    ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
    
    • 1
    • 2

    1.fd参数表示文件描述符
    2.iov参数表示指向iovec结构体数组的结构体指针
    3.iovcnt表示数组中结构体的数量

    iovec数组

    struct iovec {
        void *iov_base;    // 缓冲区的起始地址
        size_t iov_len;    // 缓冲区的长度
    };
    
    • 1
    • 2
    • 3
    • 4

    常见的使用方式:

        iov[0].iov_base = buf1; //缓冲区的起始地址
        iov[0].iov_len = strlen(buf1);//缓冲区的长度!
        iov[1].iov_base = buf2;
        iov[1].iov_len = strlen(buf2);
    
        int fd = 1;  
        ssize_t bytes_written = writev(fd, iov, 2);//将这两个缓冲区的内容全部
        //                                           写入文件描述符
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    readv函数

    用于把文件描述符中的数据一次性读到多个缓冲区中,也叫作分散读

    ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
    
    • 1

    使用方法和writev类似

    iov[0].iov_base= buf1;
    iov[0].iov_len=sizeof(buf1);
    iov[1].iov_base = buf2;
    iov[1].iov_len = sizeof(buf2);
    ssize_t bytes_read = readv(fd,iov,2);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    connect函数

    connect 函数用于建立一个客户端套接字与服务端套接字之间的连接。它在客户端套接字上调用,指示客户端要连接到指定的服务器地址和端口。

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

    1.sockfd表示客户端文件描述符
    2.sockaddr表示要连接的地址及端口等信息(服务端的IP地址和监听的端口)
    3.addrlen表示结构体的大小

    常见使用方式:

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1) {
            perror("socket");
            exit(1);
        }
    
        // 准备服务器地址信息
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(8080);  // 服务器端口
        server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 服务器IP地址
    
        // 连接到服务器
        if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
            perror("connect");
            exit(1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    fcntl函数

    fcntl 函数是一个在 Unix 和类 Unix 操作系统中使用的函数,主要用于控制文件描述符(file descriptor)的属性和执行各种操作。这包括修改文件状态标志、获取或设置文件描述符的属性、以及执行非阻塞操作等。具体来说,fcntl 函数的一些常见用途包括:

    1.修改文件状态标志:通过 fcntl 函数,你可以修改文件描述符的状态标志,例如将文件设置为非阻塞模式,以便在读写操作时不会被阻塞。这是通过设置 O_NONBLOCK 标志实现的。

    2.获取或设置文件描述符属性:你可以使用 fcntl 函数获取或设置文件描述符的各种属性,如获取或设置文件的访问模式、文件的拥有者、或文件的屏蔽字(file mode creation mask)等。

    3.复制文件描述符:你可以使用 F_DUPFD 命令来复制一个文件描述符,这会创建一个新的文件描述符,指向与原始文件描述符相同的文件。

    4.获取或设置文件锁:fcntl 函数还可用于获取或设置文件锁,以确保多个进程可以安全地访问共享文件。你可以使用 F_GETLK 命令来获取文件锁信息,或使用 F_SETLK 和 F_SETLKW 命令来设置或阻塞文件锁。

    5.取消文件锁:通过 F_SETLK 命令,你还可以用来取消现有的文件锁。

    参考链接:fcntl

    #include 
    
    int fcntl(int fd, int cmd, ... /* arg */);
    
    • 1
    • 2
    • 3

    1.fd是要操作的文件描述符
    2.cmd是对应的操作命令,如下:

    F_DUPFD:创建一个新的文件描述符,指向与原始文件描述符相同的文件。

    F_GETFD:获取文件描述符的标志。

    F_SETFD:设置文件描述符的标志。

    F_GETFL:获取文件的状态标志(如 O_RDONLY、O_WRONLY、O_NONBLOCK 等)。

    F_SETFL:设置文件的状态标志。

    F_GETOWN:获取文件描述符的所有权(如进程 ID 或进程组 ID)。

    F_SETOWN:设置文件描述符的所有权。

    F_GETLK:获取文件锁的信息。

    F_SETLK:设置文件锁,如果锁已存在则返回错误。

    F_SETLKW:设置文件锁,如果锁已存在则等待。

    使用例子:

    //对文件描述符设置非阻塞
    int setnonblocking(int fd)
    {
        int old_option = fcntl(fd, F_GETFL);
        int new_option = old_option | O_NONBLOCK;// O_NONBOLOCK为非阻塞标志.
        fcntl(fd, F_SETFL, new_option);
        return old_option;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因为文件描述符的标志是一个位掩码,所以必须要先获取原来的状态,再跟新状态或运算,才可以修改文件描述符的状态.

    stat函数

     #include 
     #include 
     #include 
     
     //获取文件属性,存储在statbuf中
     int stat(const char *pathname, struct stat *statbuf);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.参数pathname表示一个完整的文件路径
    2.参数statbuf是一个stat的结构体指针,用来在调用stat函数之后,将文件的信息存储在这个结构体中

    struct stat 
     {
       mode_t    st_mode;        /* 文件类型和权限 */
       off_t     st_size;        /* 文件大小,字节数*/
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    用stat函数可以获取文件的类型,权限,以及大小等信息,常用于判断文件是否存在,可读,或者是不是目录等

    常见的使用方式:

    查看路径是否有文件存在:

     if(stat(m_real_file,&m_file_stat)<0) 
     //文件不存在
    
    • 1
    • 2

    查看文件是否有可读权限

    if(!(m_file_stat.st_mode&S_IROTH)) 
    //不可读
    
    • 1
    • 2

    查看该路径是不是目录

    if(S_ISDIR(m_file_stat.st_mode))
    //是目录
    
    • 1
    • 2

    mmap函数

    用于将一个文件或其他对象映射到内存,提高文件的访问速度。

    void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
    int munmap(void* start,size_t length);
    
    • 1
    • 2

    start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址

    length:映射区的长度

    prot:期望的内存保护标志,不能与文件的打开模式冲突

    PROT_READ 表示页内容可以被读取

    flags:指定映射对象的类型,映射选项和映射页是否可以共享

    MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件

    fd:有效的文件描述符,一般是由open()函数返回

    off_toffset:被映射对象内容的起点

    常见的使用方式:

     int fd=open(m_real_file,O_RDONLY);
        m_file_address=(char*)mmap(0,m_file_stat.st_size,PROT_READ,MAP_PRIVATE,fd,0);
    
    • 1
    • 2

    setsockopt

    该函数用于设置套接字选项,可以对套接字进行一系列属性设置,比如超时时间、缓冲区大小、重传参数等。这样可以根据具体需求来调整网络通信的行为,以优化程序性能或实现特定功能。

    int setsockopt(int sockfd, int level, int optname,
                   const void *optval, socklen_t optlen);
    
    • 1
    • 2
    1. sockfd表示要操作的文件描述符

    2. level:表示选项的类型,可以是 SOL_SOCKET、SOL_TCP、SOL_UDP 等等。不同的协议族定义了不同的选项类型。

    SOL_SOCKET:表示通用套接字选项,适用于所有套接字类型。
    SOL_TCP:表示 TCP 协议相关选项。
    SOL_UDP:表示 UDP 协议相关选项。

    1. optname:表示具体要设置的选项名。比如,SO_KEEPALIVE 用于启用或禁用 TCP 的 keepalive 特性,SO_RCVBUF 用于设置接收缓冲区大小等等。

    SO_KEEPALIVE:设置是否启用 TCP keepalive。
    SO_RCVBUF:设置接收缓冲区大小。
    SO_REUSEADDR:允许重用本地地址。
    O_LINGER:用于控制套接字关闭时的行为。设置 SO_LINGER 选项时通常需要提供一个 struct linger 结构体,其中包含两个字段:
    l_onoff:控制 SO_LINGER 选项是否生效,0 表示关闭 SO_LINGER,非0 表示开启。
    l_linger:表示关闭套接字时要等待的时间,单位为秒。

    struct linger {
        int l_onoff;  // 是否启用延迟关闭
        int l_linger; // 延迟关闭的时间,单位为秒
    };
    
    • 1
    • 2
    • 3
    • 4
    1. optval:是一个指向存放选项值的缓冲区的指针。根据不同选项的类型,这个缓冲区可能需要指向不同类型的数据结构。

    optlen:表示参数 optval 缓冲区的大小。

    struct linger tmp = {0, 1};
            setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));//
            套接字延迟1S后关闭(调用close函数之后)
    //以确保尚未发送的数据得以发送。这样可以避免数据丢失
    
    • 1
    • 2
    • 3
    • 4
    int flag = 1;
        setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    
    • 1
    • 2

    当一个套接字关闭后,它所占用的端口并不会立即释放,在一段时间内仍然会被操作系统保留(TIME_WAIE状态)。此时如果另一个套接字尝试绑定同一端口时,如果没有使用 SO_REUSEADDR 选项,将会得到一个端口被占用的错误。

    使用 SO_REUSEADDR 选项后,新的套接字可以在之前套接字关闭的情况下,尽管还有一段时间端口处于 TIME_WAIT 状态,但仍然可以绑定相同端口。这样可以使得服务器能够快速重新启动,而不必等待一段时间。

    相当于是内核开启了 tcp_tw_reuse参数,可以快速复用处于TIME_WAIT状态的连接,跳过2MSL的时间


    epoll相关函数

    epoll是linux操作系统,内核提供给用户态专门用于多路复用的系统调用函数,其作用是可以让一个进程维护多个socket.

    epoll的流程
    1.使用epoll_create函数创建一个指向内核事件表的文件描述符

    2.使用epoll_ctl函数将想要监听的socket和想要监听的事件类型注册到epoll上

    3.使用epoll_wait函数等待事件到达,进程/线程通过对应的事件处理方式处理事件

    epoll_create

    #include 
    int epoll_create(int size)
    
    • 1
    • 2

    作用:创建一个指向epoll内核事件表的文件描述符,返回值用于epoll其他函数的第一个参数

    epoll_ctl函数

    #include 
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    
    • 1
    • 2

    用于将文件描述符注册到epoll上,或者对已经注册好的文件描述符修改和删除

    1.第一个参数是epoll_create函数的句柄
    2.第二个参数是一个命令,分别用三个宏表示注册,修改,删除

    EPOLL_CTL_ADD (注册新的fd到epfd),
    EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
    EPOLL_CTL_DEL (从epfd删除一个fd);

    3.event参数表示要监听的事件

    epoll_event结构体

    struct epoll_event {
    __uint32_t events; //表示事件的类型
    epoll_data_t data; //
    };
    
    • 1
    • 2
    • 3
    • 4

    events对应的事件类型有如下几种:
    EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

    EPOLLOUT:表示对应的文件描述符可以写

    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

    EPOLLERR:表示对应的文件描述符发生错误

    EPOLLHUP:表示对应的文件描述符被挂断;

    EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的

    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里


    epoll_data_t是一个共用体(联合体)表示用户数据,用来存储额外的信息

    typedef union epoll_data {
        void *ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ptr:一个指向 void 类型的指针,通常用于关联一个任意类型的指针。
    fd:一个整数,通常用于关联一个文件描述符(比如套接字描述符)。
    u32:一个32位的无符号整数。
    u64:一个64位的无符号整数。

    epoll_ctl常见的使用方式:(这里如果看不太懂events下面还有详解)

    注册:

         epoll_event event;
         event.data.fd = fd;//设置文件描述符!
     #ifdef ET
         event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
     8#endif
     
    #ifdef LT
        event.events = EPOLLIN | EPOLLRDHUP;
    #endif
    
        if (one_shot)
            event.events |= EPOLLONESHOT;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
        setnonblocking(fd);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    删除:

    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
       close(fd);
    
    • 1
    • 2

    修改

    void modfd(int epollfd, int fd, int ev)
     {
         epoll_event event;
         event.data.fd = fd;
     
     #ifdef ET
         event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
     #endif
     
    #ifdef LT
        event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
    #endif
    
        epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    epoll_wait函数

    用于等待事件的发生,当监控的文件描述符上有事件发生时,返回有事件发生的文件描述符的个数,通知进程处理事件

    #include 
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • 1
    • 2

    1.epfd是epoll_wait函数创建的句柄
    2.events表示内核得到的事件的集合
    3.maxevents表示events的大小,即能够处理的最大事件数
    4.timeout表示超时时间:
    -1:阻塞
    0:非阻塞
    大于0:指定毫秒数

    常见的使用方式:

    int epfd = epoll_create(1); // 创建 epoll 实例
    struct epoll_event events[MaxEvents]; // 用于存储事件的数组
    
    // 将需要监听的文件描述符添加到 epoll 实例(epfd)中,使用 epoll_ctl 函数。
    
    int num_events = epoll_wait(epfd, events, MaxEvents, timeout);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    epoll_ctl函数和epoll_wait函数中的events详解:

    epoll_ctl 函数:
    events 参数用于指定你希望监听的事件,这个参数是用于告诉 epoll 实例需要监听哪些事件的。在调用 epoll_ctl 函数时,你需要为 events 参数赋值,指定感兴趣的事件类型,如 EPOLLIN(可读事件)或 EPOLLOUT(可写事件)等。
    events 参数通常是一个位掩码,可以使用位运算来指定多个事件,例如 EPOLLIN | EPOLLOUT 表示同时监听可读和可写事件。
    events 参数的角色是告诉 epoll 实例你关心的事件类型以及要监听的文件描述符。

    epoll_wait 函数:
    events 参数用于接收 epoll_wait 函数返回的已发生事件的信息。在调用 epoll_wait 之前,你不需要为 events 参数赋值,因为它将由 epoll_wait 函数填充。
    当 epoll_wait 函数返回时,它会将已发生的事件信息填充到 events 数组中。你可以检查每个事件的类型和相关的文件描述符,以确定发生了什么事件。

    pthread相关函数

    pthread_create

    #include 
    int pthread_create (pthread_t *thread_tid,                 //返回新生成的线程的id
                        const pthread_attr_t *attr,         //指向线程属性的指针,通常设置为NULL
                        void * (*start_routine) (void *),   //处理线程函数的地址
                        void *arg);                         //start_routine()中的参数
    
    • 1
    • 2
    • 3
    • 4
    • 5

    pthread_create是C语言中,用于创建线程的函数
    其中
    thread_tid 是一个指向pthread_t类型的指针,用于存储新创建线程的标识符(pthread_t类型也被成为线程标识符)

    attr 是一个指向pthread_attr_t类型的指针,用于指定线程的属性,通常为NULL

    start_routine是一个指向返回类型为void * ,参数类型为(void *)的一个函数指针,该函数是线程要执行的函数,通常把线程的入口函数放在这里

    arg 是一个 void类型的指针,作为start_routine函数中的参数,在新线程启动时,会作为参数传递给start_routine函数

    当调用pthread_create函数之后,会在操作系统中创建一个新的线程,新的线程会直接去执行start_routine指向的入口函数.新线程可以和主线程同时执行.各自执行不同的代码路径

    返回值为0表示线程创建成功,否则表示线程创建失败.

    pthread_join

    int pthread_join(pthread_t thread, void **retval);
    
    • 1

    thread表示要等待的线程标识符
    retval 是一个指向指针的指针,用于接受等待线程的返回值

    主线程在调用pthread_join函数之后,会被阻塞,直到被等待的线程结束,同时,retval指向的指针将获得等待线程的返回值(退出状态).,如果不关心等待线程的返回值,可以将retval参数设置为NULL.

    该函数主要用于线程间的同步操作,以确保主线程在等待子线程运行结束之后再继续执行
    返回值为0表示函数使用成功,否则失败

    pthread_detach

    #include 
    int pthread_detach(pthread_t thread);
    
    • 1
    • 2

    pthread_detach是C语言中用于 线程分离的函数
    其中 thread 是 pthread_t类型的标识符,用于指定被分离的线程
    线程分离指的是让该线程与其主线程分离
    在线程与其主线程分离之后,主线程就不再等待该线程的结束,不需要主线程调用pthread_join函数来等待该线程的终止并且该线程在结束时也会自动释放资源,相当于是让该线程与主线程脱离联系.

    当线程终止后,会释放一些资源:
    1.线程的栈空间释放:现成的栈空间用来存放局部变量,函数调用等,线程终止后会自动释放这些资源,供其他线程使用
    2.线程描述符的释放:线程描述符是内核为线程分配的数据结构,用来跟踪线程的状态、优先级等信息。当一个线程终止时,其线程描述符会被释放,以允许新的线程创建并使用该描述符。

    线程分离的意义是
    主线程不需要在结束时显示的等待子线程的结束,只要使用了线程分离,那么子线程会自行结束也会自行释放资源,如果一个主线程不关心子线程的返回值和子线程的结束场景,我们就可以使用线程分离来提高代码的简洁性

    pthread_exit

    void pthread_exit(void *retval);
    
    • 1

    当线程执行完毕时,可以通过pthread_exit函数来终止自身
    其中retval 是指向需要传递给等待线程的退出状态的指针.

    参数retval 可以被传递给等待该线程的其他线程的pthread_join函数,作为该线程的退出状态

    通俗来讲,就是你可以在线程结束之前,动态开辟创建一个指针,用它来存储和返回线程的退出状态,该线程在终止时,就会返回这个退出状态,相当于是线程的返回值;当我们在其他的线程中使用pthread_join函数等待这个线程结束时,这个线程的退出状态就会保存在pthread_join的第二个参数里,相当于是线程退出状态的一个传递.

    int *result = malloc(sizeof(int));
        *result = 42;
        pthread_exit((void *)result);
    
    • 1
    • 2
    • 3

    调用pthread_exit函数会立刻终止当前正在执行的线程,并且将控制权交还给主线程,如果被终止的线程是主线程,则会终止整个程序,如果不关心线程的退出状态,retval设置为NULL

    pthread_exit(NULL); //不关心线程的退出状态
    
    • 1

    最常见的用法是线程在执行完任务后调用 pthread_exit 来退出。若线程没有调用 pthread_exit,则当线程函数返回时,线程将自动调用 pthread_exit 并将返回值设为 NULL。同时允许传递退出状态给等待线程的函数,

    pthread_join和pthread_exit连用的案例:

    #include 
    #include 
    #include 
    
    void *thread_function(void *arg) {
        // 获取整数参数值
        int *value = (int *)arg;
    
        printf("Thread %d is running!\n", *value);
    
        // 模拟线程执行任务...
        // ...
    
        // 分配并返回退出状态
        int *result = malloc(sizeof(int));
        *result = 42;
        pthread_exit((void *)result);
    }
    
    int main() {
        pthread_t thread;
        int thread_arg = 1;
        void *thread_result;
    
        // 创建线程
        if (pthread_create(&thread, NULL, thread_function, (void *)&thread_arg) != 0) {
            fprintf(stderr, "Failed to create thread\n");
            return 1;
        }
    
        // 等待线程结束并获取退出状态
        if (pthread_join(thread, &thread_result) != 0) {
            fprintf(stderr, "Failed to join thread\n");
            return 1;
        }
    
        printf("Thread exited with result: %d\n", *(int *)thread_result);
    
        // 释放退出状态的内存
        free(thread_result);
    
        return 0;
    }
    
    • 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

    MYSQL相关函数

    MYSQL相关函数

    信号量,互斥锁,条件变量相关函数

    信号量,互斥锁,条件变量相关函数

    time时间戳相关函数和结构体

    time_t结构体和time函数

    time_t time(time_t *seconds);
    
    • 1

    time_t 是Unix时间戳,代表从1970年1月1日距离当前时间的秒数,如果想把这个时间戳存起来,就可以给这个函数传入一个time_t结构体的指针作参数,时间戳就会保存在time_t里,如果不想存,参数设置为NULL即可.

    tm结构体和localtime函数

    struct tm {
        int tm_sec;   // 秒,范围从 0 到 59
        int tm_min;   // 分,范围从 0 到 59
        int tm_hour;  // 时,范围从 0 到 23
        int tm_mday;  // 一月中的日,范围从 1 到 31
        int tm_mon;   // 月,范围从 0 到 11
        int tm_year;  // 年份-1900
        int tm_wday;  // 一周中的日,范围从 0(周日)到 6(周六)
        int tm_yday;  // 一年中的日,范围从 0 到 365
        int tm_isdst; // 夏令时标识符
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    tm结构体也是用来保存时间的,其中包括年月日时分秒

    truct tm *localtime(const time_t *time);
    
    • 1

    localtime函数可以把一个 time_t的指针,转化为tm类型的指针,其目的是把time_t时间戳,转化为人为可以观测的时间

    timeval结构体和gettimeofday

    struct timeval {
        time_t tv_sec;  // 秒
        suseconds_t tv_usec;  // 微秒
    };
    
    • 1
    • 2
    • 3
    • 4

    timeval也是一个存放时间的结构体,不过它相对于time_t来说更加精细,其中包括秒和微秒

    int gettimeofday(struct timeval *restrict tv, struct timezone *restrict tz);
    
    • 1

    该函数接受两个参数,第一个参数是指向 struct timeval 结构体的指针,用来存储获取的时间值;第二个参数是 struct timezone 结构体的指针,用于支持遗留代码,通常传入 NULL 即可。

    通过调用 gettimeofday() 函数,可以获取当前时间的秒数和微秒数,存储在指定的 struct timeval 结构体中。

    文件操作函数

    文件操作函数

    flush函数和fflush函数

    #include 
    int fflush(FILE *stream);
    
    • 1
    • 2

    刷新某一个指定流的缓冲区

    ostream& flush();
    
    • 1

    无参数,刷新所有缓冲区

  • 相关阅读:
    Linux shell
    利用化合物名称从PubChempy中批量下载化合物信息
    windows bat批处理文件,实现某个软件的重启
    Tips for training DNN
    计算机毕业设计之java+javaweb的学生信息管理系统
    JVM类加载机制
    Flask狼书笔记 | 09_图片社交网站 - 大型项目的架构与需求
    高防CDN有什么作用?
    DC系列靶机4通关教程
    助力商家高效接单发货,震坤行物流服务再升级
  • 原文地址:https://blog.csdn.net/qq_52508038/article/details/133824148