线程竞争
那么初始化一个整型为 0,使用一万个线程,每个线程都对该整型加 1,最后结果不一定会是 10000。这是因为整型变量的赋值操作不是原子操作,也就是说它不是一个不可分割的操作,而是由多条指令组成的。例如,对一个整型变量执行 a++
操作,实际上包含了三个步骤:
- 将变量 a 的值从内存中读取到寄存器中。
- 将寄存器中的值加 1。
- 将寄存器中的值写回到内存中。
-
在多线程环境中,如果没有同步机制,那么这三个步骤可能会被打断或重排,导致数据不一致或丢失。例如,假设有两个线程 A 和 B 同时对变量 a 执行
a++
操作,初始时 a 的值为 0,那么可能出现以下情况:- 线程 A 执行第一步,将 a 的值读取到寄存器中,此时寄存器中的值为 0。
- 线程 B 执行第一步,将 a 的值读取到寄存器中,此时寄存器中的值也为 0。
- 线程 A 执行第二步,将寄存器中的值加 1,此时寄存器中的值为 1。
- 线程 B 执行第二步,将寄存器中的值加 1,此时寄存器中的值也为 1。
- 线程 A 执行第三步,将寄存器中的值写回到内存中,此时内存中 a 的值为 1。
- 线程 B 执行第三步,将寄存器中的值写回到内存中,此时内存中 a 的值也为 1。
-
这样就出现了一个问题,虽然两个线程都对 a 加了 1,但是最后 a 的值只增加了 1。这就是多线程操作共享变量时可能出现的竞态条件(race condition),也就是多个线程同时访问和修改同一个数据时导致结果不正确或不可预测的情况。
-
要避免这个问题,有两种常见的方法:
- 使用原子操作(atomic operation),也就是一种不可分割的操作,它可以保证在执行过程中不会被其他线程打断或干扰。C++ 提供了
<atomic>
头文件来支持原子操作。例如,可以使用std::atomic<int>
类型来声明一个原子整型变量,并使用fetch_add
方法来对其进行原子加法操作。这样就可以保证每个线程都能正确地对该变量加 1,并返回旧值或新值。
cpp#include <atomic> #include <thread> #include <iostream> std::atomic<int> a(0); // 声明一个原子整型变量,并初始化为 0 void add_one() { // 对原子整型变量执行原子加法操作,并返回旧值 int old = a.fetch_add(1); // 输出旧值和新值 std::cout << "Old value: " << old << ", new value: " << old + 1 << std::endl; } int main() { // 创建一万个线程 std::thread threads[10000]; for (int i = 0; i < 10000; i++) { threads[i] = std::thread(add_one); } // 等待所有线程结束 for (int i = 0; i < 10000; i++) { threads[i].join(); } // 输出最终结果 std::cout << "Final value: " << a << std::endl; return 0; }
- 使用互斥锁(mutex),也就是一种同步机制,它可以保证在某一时刻只有一个线程可以访问和修改共享数据。C++ 提供了
<mutex>
头文件来支持互斥锁。例如,可以使用std::mutex
类型来声明一个互斥锁,并使用lock
和unlock
方法来对其进行加锁和解锁操作。这样就可以保证每个线程在对共享变量加 1 时,不会被其他线程干扰。
cpp#include <mutex> #include <thread> #include <iostream> int a = 0; // 声明一个普通整型变量,并初始化为 0 std::mutex m; // 声明一个互斥锁 void add_one() { // 对互斥锁进行加锁 m.lock(); // 对普通整型变量执行加法操作 a++; // 输出当前值 std::cout << "Current value: " << a << std::endl; // 对互斥锁进行解锁 m.unlock(); } int main() { // 创建一万个线程 std::thread threads[10000]; for (int i = 0; i < 10000; i++) { threads[i] = std::thread(add_one); } // 等待所有线程结束 for (int i = 0; i < 10000; i++) { threads[i].join(); } // 输出最终结果 std::cout << "Final value: " << a << std::endl; return 0; }
- 使用原子操作(atomic operation),也就是一种不可分割的操作,它可以保证在执行过程中不会被其他线程打断或干扰。C++ 提供了
-
原子操作的优点是效率高,不需要额外的同步开销,但是它只能对简单的数据类型进行操作,而且不能保证内存顺序性(memory order),也就是说多个原子操作之间的执行顺序可能会被编译器或处理器重排,导致结果不符合预期。互斥锁的优点是灵活性高,可以对任意复杂的数据类型进行操作,而且可以保证内存顺序性,但是它需要额外的同步开销,而且可能导致死锁(deadlock),也就是说多个线程相互等待对方释放锁,导致程序无法继续执行。
除了原子操作和互斥锁之外,还有一些其他的方法可以避免多线程操作共享变量时出现的问题,例如:
-
使用条件变量(condition variable),也就是一种同步机制,它可以让一个线程等待另一个线程的通知,从而避免不必要的轮询或竞争。条件变量通常和互斥锁一起使用,以保证数据的一致性。C++ 提供了
<condition_variable>
头文件来支持条件变量。例如,可以使用std::condition_variable
类型来声明一个条件变量,并使用wait
和notify_one
或notify_all
方法来进行等待和通知操作。这样就可以实现生产者-消费者模式,也就是一个线程负责生产数据,另一个线程负责消费数据,两者之间通过条件变量进行协调。cpp#include <mutex> #include <condition_variable> #include <queue> #include <thread> #include <iostream> std::mutex m; // 声明一个互斥锁 std::condition_variable cv; // 声明一个条件变量 std::queue<int> q; // 声明一个队列 bool done = false; // 声明一个标志位 void producer() { // 生产 10 个数据 for (int i = 0; i < 10; i++) { // 对互斥锁进行加锁 std::unique_lock<std::mutex> lock(m); // 向队列中插入数据 q.push(i); // 输出生产的数据 std::cout << "Produced " << i << std::endl; // 对互斥锁进行解锁 lock.unlock(); // 通知消费者 cv.notify_one(); } // 设置标志位为 true done = true; // 通知消费者 cv.notify_one(); } void consumer() { while (true) { // 对互斥锁进行加锁 std::unique_lock<std::mutex> lock(m); // 等待生产者的通知或标志位为 true cv.wait(lock, []{return !q.empty() || done;}); // 如果队列不为空,则取出数据并消费 if (!q.empty()) { int x = q.front(); q.pop(); std::cout << "Consumed " << x << std::endl; lock.unlock(); } else { // 如果队列为空且标志位为 true,则退出循环 if (done) { break; } lock.unlock(); } } } int main() { // 创建两个线程 std::thread t1(producer); std::thread t2(consumer); // 等待两个线程结束 t1.join(); t2.join(); return 0; }
-
使用信号量(semaphore),也就是一种同步机制,它可以限制对共享资源的访问数量,从而避免过载或冲突。信号量有一个整数值,表示可用的资源数量,当一个线程想要访问资源时,它必须先获取信号量,如果信号量大于零,则信号量减一,并允许访问资源;如果信号量等于零,则线程必须等待其他线程释放信号量。当一个线程访问完资源后,它必须释放信号量,使信号量加一,并唤醒等待的线程。C++ 提供了
<semaphore>
头文件来支持信号量。例如,可以使用std::counting_semaphore
类型来声明一个计数信号量,并使用acquire
和release
方法来进行获取和释放操作。这样就可以实现多个线程同时访问有限数量的资源。cpp#include <semaphore> #include <thread> #include <iostream> std::counting_semaphore<3> sem(3); // 声明一个计数信号量,初始值为 3 void access_resource(int id) { // 获取信号量 sem.acquire(); // 输出访问资源的线程 id std::cout << "Thread " << id << " is accessing resource" << std::endl; // 模拟访问资源的时间 std::this_thread::sleep_for(std::chrono::seconds(1)); // 输出释放资源的线程 id std::cout << "Thread " << id << " is releasing resource" << std::endl; // 释放信号量 sem.release(); } int main() { // 创建 10 个线程 std::thread threads[10]; for (int i = 0; i < 10; i++) { threads[i] = std::thread(access_resource, i); } // 等待 10 个线程结束 for (int i = 0; i < 10; i++) { threads[i].join(); } return 0; }
IO多路复用
-
IO 多路复用:IO 多路复用是一种技术,它可以让一个进程或线程同时监视多个 IO 事件(如文件描述符、套接字等),并在其中一个或多个 IO 事件发生时,通知该进程或线程进行相应的处理。IO 多路复用可以提高 IO 效率,避免不必要的阻塞和轮询,适用于高并发的网络编程场景。
-
IO 多路复用的原理:IO 多路复用的原理是利用操作系统提供的一些系统调用(如 select, poll, epoll 等),将多个 IO 事件注册到一个事件集合中,然后让操作系统负责监视这些事件的状态变化,并在有事件发生时返回给用户程序。用户程序只需要调用一次系统调用,就可以处理多个 IO 事件,而不需要自己去轮询每个 IO 事件的状态,从而节省了 CPU 资源和时间。
-
IO 多路复用的优缺点:IO 多路复用的优点是它可以实现高效的 IO 处理,减少了进程或线程的切换开销,提高了并发性能。IO 多路复用的缺点是它需要额外的系统调用开销,而且不同的系统调用有各自的局限性和兼容性问题。
-
Linux 中常见的 IO 多路复用系统调用:Linux 中常见的 IO 多路复用系统调用有以下几种:
- select:select 是最早出现的 IO 多路复用系统调用,它可以监视一组文件描述符(file descriptor),并在其中一个或多个文件描述符就绪(可读、可写或有异常)时返回。select 的原型如下:
cpp#include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中,nfds 是要监视的文件描述符的数量,一般为最大文件描述符值加一;readfds, writefds, exceptfds 分别是指向读、写、异常文件描述符集合的指针,如果对某种类型不感兴趣,可以传入 NULL;timeout 是指定等待时间的指针,如果为 NULL,则表示无限等待;如果为零,则表示立即返回;否则表示等待指定的秒数和微秒数。select 的返回值是就绪文件描述符的数量,如果超时则返回零,如果出错则返回 -1,并设置 errno。
select 的优点是它可以跨平台使用,兼容性好;缺点是它只能监视 1024 个文件描述符(受 FD_SETSIZE 的限制),而且每次调用都需要将文件描述符集合从用户空间拷贝到内核空间,效率低。
- poll:poll 是对 select 的改进,它也可以监视一组文件描述符,并在其中一个或多个文件描述符就绪时返回。poll 的原型如下:
cpp#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其中,fds 是指向一个 pollfd 结构体数组的指针,每个 pollfd 结构体包含了一个文件描述符和一个事件掩码(表示要监视哪些事件);nfds 是要监视的文件描述符的数量;timeout 是指定等待时间的毫秒数,如果为 -1,则表示无限等待;如果为零,则表示立即返回;否则表示等待指定的毫秒数。poll 的返回值是就绪文件描述符的数量,如果超时则返回零,如果出错则返回 -1,并设置 errno。
poll 的优点是它没有监视文件描述符的数量限制,而且不需要每次调用都重新设置文件描述符集合;缺点是它仍然需要将整个文件描述符数组从用户空间拷贝到内核空间,而且返回时需要遍历整个数组来找出就绪的文件描述符,效率低。
- epoll:epoll 是 Linux 特有的 IO 多路复用系统调用,它可以监视一组文件描述符,并在其中一个或多个文件描述符就绪时返回。epoll 的原型如下:
cpp#include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
其中,epoll_create 用于创建一个 epoll 实例,并返回一个文件描述符 epfd,size 参数已经被忽略,只是为了兼容旧版本的接口;epoll_ctl 用于向 epoll 实例中添加、修改或删除一个文件描述符,op 参数表示操作类型(EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL),fd 参数表示要操作的文件描述符,event 参数是指向一个 epoll_event 结构体的指针,该结构体包含了一个事件掩码和一个用户数据(可以是一个指针或一个整数);epoll_wait 用于等待 epoll 实例中的文件描述符就绪,并将就绪的文件描述符填充到 events 数组中,maxevents 参数表示 events 数组的大小,timeout 参数表示等待时间的毫秒数,如果为 -1,则表示无限等待;如果为零,则表示立即返回;否则表示等待指定的毫秒数。epoll_wait 的返回值是就绪文件描述符的数量,如果超时则返回零,如果出错则返回 -1,并设置 errno。
epoll 的优点是它使用了内核与用户空间共享内存的机制,避免了不必要的拷贝开销,而且它只返回就绪的文件描述符,不需要遍历整个集合,效率高;缺点是它只能在 Linux 上使用,而且对于某些特殊的文件描述符(如 pipe 的写端),它可能产生惊群效应(thundering herd),也就是说多个线程都被唤醒,但只有一个线程能够处理事件,其他线程又重新进入等待状态。
进程间通通信(IPC)的方法。
-
进程间通信:进程间通讯是指不同的进程之间如何传递和共享数据的技术,它可以实现进程之间的协作和同步,提高系统的并发性能和可靠性。Linux 操作系统中提供了多种进程间通讯的方法,主要有以下几种:
- 管道(pipe) :管道是一种最简单的进程间通讯的方法,它可以在有亲缘关系的进程(如父子进程)之间建立一个单向的数据流,一个进程向管道的写端写入数据,另一个进程从管道的读端读取数据。管道的优点是使用方便,不需要额外的文件或内存空间;缺点是只能在有亲缘关系的进程之间使用,而且只能实现单向的通讯。Linux 提供了
pipe
系统调用来创建一个管道,并返回两个文件描述符,分别表示管道的读端和写端。
cpp#include <unistd.h> int pipe(int pipefd[2]);
- 命名管道(named pipe) :命名管道是对管道的改进,它可以在没有亲缘关系的进程之间建立一个双向的数据流,它通过在文件系统中创建一个特殊的文件来实现,任何知道该文件名的进程都可以打开该文件,并进行读写操作。命名管道的优点是可以在任意的进程之间使用,而且可以实现双向的通讯;缺点是需要额外的文件空间,而且数据传输速度较慢。Linux 提供了
mkfifo
系统调用来创建一个命名管道,并返回一个文件描述符。
cpp#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
- 消息队列(message queue) :消息队列是一种更高级的进程间通讯的方法,它可以在没有亲缘关系的进程之间传递一组消息,每个消息都有一个类型和一个长度,消息队列按照先进先出(FIFO)或者优先级的顺序存储和发送消息。消息队列的优点是可以避免数据同步和阻塞问题,而且可以根据消息类型进行选择性地接收;缺点是需要额外的内核空间,而且消息格式和长度有限制。Linux 提供了
msgget
,msgsnd
和msgrcv
等系统调用来创建、发送和接收消息队列。
cpp#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg); int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 管道(pipe) :管道是一种最简单的进程间通讯的方法,它可以在有亲缘关系的进程(如父子进程)之间建立一个单向的数据流,一个进程向管道的写端写入数据,另一个进程从管道的读端读取数据。管道的优点是使用方便,不需要额外的文件或内存空间;缺点是只能在有亲缘关系的进程之间使用,而且只能实现单向的通讯。Linux 提供了
进程崩溃(crash)的原因和处理方法
-
进程崩溃:进程崩溃是指一个进程在运行过程中由于某些原因意外地终止,无法正常地完成其任务。进程崩溃通常会导致系统资源的浪费,用户数据的丢失,甚至系统的不稳定或死机。
-
进程崩溃的原因:进程崩溃的原因有很多,主要有以下几种:
- 非法操作:如果一个进程试图执行一些非法的操作,例如访问非法的内存地址,除以零,执行无效的指令等,那么操作系统会检测到这些错误,并向该进程发送一个信号(signal),通常是 SIGSEGV(段错误),SIGFPE(浮点异常),SIGILL(非法指令)等。如果该进程没有注册相应的信号处理函数(signal handler),那么该进程就会被终止,并生成一个核心转储文件(core dump file),用于记录进程崩溃时的内存和寄存器状态。
- 资源不足:如果一个进程在运行过程中需要申请更多的资源,例如内存,文件描述符,线程等,而操作系统无法满足其需求,那么操作系统也会向该进程发送一个信号,通常是 SIGXFSZ(文件大小超限),SIGXCPU(CPU 时间超限),ENOMEM(内存不足)等。如果该进程没有注册相应的信号处理函数,那么该进程也会被终止,并可能生成一个核心转储文件。
- 程序逻辑错误:如果一个进程在运行过程中出现了程序逻辑错误,例如死循环,死锁,内存泄漏等,那么该进程可能会陷入无法正常退出的状态,或者消耗过多的系统资源,影响其他进程的运行。这种情况下,操作系统不一定会主动终止该进程,而是需要用户或管理员手动地杀死(kill)该进程,并分析其日志文件(log file)或调试信息(debug info)来找出错误原因。
-
进程崩溃的处理方法:进程崩溃的处理方法有以下几种:
- 预防:预防是最好的处理方法,它要求程序员在编写程序时遵循良好的编码规范和风格,避免使用不安全的函数或语句,检查各种可能的边界条件和异常情况,并使用合适的工具和方法进行调试和测试,以消除程序中潜在的错误和漏洞。
- 恢复:恢复是在进程崩溃后尽可能地恢复其正常状态或最小化其损失的方法,它要求程序员在编写程序时注册合适的信号处理函数,并在信号处理函数中执行一些必要的操作,例如释放资源,保存数据,记录日志等。此外,还可以使用一些工具和方法来分析核心转储文件或日志文件,以确定进程崩溃时的上下文信息和错误原因,并根据这些信息进行修复或优化。
- 重启:重启是在进程崩溃后重新启动该进程或整个系统的方法,它是一种简单而暴力的处理方法,它可以快速地恢复服务或功能,但是它不能解决根本的问题,而且可能会导致数据丢失或不一致。重启可以通过手动或自动的方式来实现,例如使用 shell 命令,编写守护进程(daemon process),使用 systemd 服务等。
套接字
-
套接字 :套接字是一种通信机制,它可以在不同的进程或不同的主机之间进行数据交换,类似于文件描述符(file descriptor),它也是一个整数,表示一个打开的通信端点。套接字的头文件是
<sys/socket.h>
,它提供了一些函数和数据结构来创建和操作套接字。 -
套接字的类型:Linux 操作系统中支持以下几种类型的套接字:
- 流式套接字(stream socket) :流式套接字使用 TCP 协议来提供可靠的、有序的、双向的字节流服务,它可以保证数据不会丢失或重复,也不会出现边界问题。流式套接字适用于需要高可靠性的应用,例如文件传输,远程登录等。流式套接字的类型常量是
SOCK_STREAM
。 - 数据报套接字(datagram socket) :数据报套接字使用 UDP 协议来提供无连接的、不可靠的、无序的数据报服务,它不保证数据的到达或顺序,也不进行重传或确认,但是它可以避免连接建立和维护的开销,提高传输效率。数据报套接字适用于需要高效率或实时性的应用,例如视频流,语音通话等。数据报套接字的类型常量是
SOCK_DGRAM
。 - 原始套接字(raw socket) :原始套接字使用 IP 协议来提供直接访问网络层的服务,它可以自定义或修改 IP 数据包的内容和格式,也可以处理一些特殊的协议,例如 ICMP,IGMP 等。原始套接字适用于需要高灵活性或定制性的应用,例如网络诊断,网络安全等。原始套接字的类型常量是
SOCK_RAW
。
- 流式套接字(stream socket) :流式套接字使用 TCP 协议来提供可靠的、有序的、双向的字节流服务,它可以保证数据不会丢失或重复,也不会出现边界问题。流式套接字适用于需要高可靠性的应用,例如文件传输,远程登录等。流式套接字的类型常量是
-
套接字的地址 :每个套接字都有一个地址(address),用于标识通信的源和目的地。不同类型的套接字有不同格式的地址,但是它们都使用一个通用的数据结构来表示,即
sockaddr
结构体,它定义如下:
cpp
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址信息
};
其中,sa_family 表示地址族(address family),也就是通信协议的类型,常见的地址族有 AF_INET
(IPv4 协议),AF_INET6
(IPv6 协议),AF_UNIX
(Unix 域协议)等;sa_data 表示地址信息,也就是具体的通信地址,它根据不同的地址族有不同的含义和格式。
为了方便使用不同地址族的地址信息,Linux 操作系统还提供了一些专门针对某种地址族的数据结构来表示地址,例如:
- 对于 IPv4 地址族(
AF_INET
),使用sockaddr_in
结构体来表示地址,它定义如下:
cpp
struct sockaddr_in {
sa_family_t sin_family; // 地址族
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IP 地址
};
其中,sin_family 表示地址族,必须为 AF_INET
;sin_port 表示端口号,使用网络字节序(big-endian)表示;sin_addr 表示 IP 地址,使用一个 in_addr
结构体表示,它定义如下:
cpp
struct in_addr {
uint32_t s_addr; // IP 地址
};
其中,s_addr 表示 IP 地址,使用网络字节序表示。为了方便地将 IP 地址和端口号转换为字符串或数字,Linux 操作系统提供了一些函数,例如 inet_ntop
,inet_pton
,htons
,ntohs
等。
- 对于 IPv6 地址族(
AF_INET6
),使用sockaddr_in6
结构体来表示地址,它定义如下:
cpp
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族
in_port_t sin6_port; // 端口号
uint32_t sin6_flowinfo; // 流信息
struct in6_addr sin6_addr; // IP 地址
uint32_t sin6_scope_id; // 作用域
};
其中,sin6_family 表示地址族,必须为 AF_INET6
;sin6_port 表示端口号,使用网络字节序表示;sin6_flowinfo 表示流信息,用于区分同一主机上的不同流;sin6_addr 表示 IP 地址,使用一个 in6_addr
结构体表示,它定义如下:
cpp
struct in6_addr {
unsigned char s6_addr[16]; // IP 地址
};
其中,s6_addr 表示 IP 地址,使用网络字节序表示。为了方便地将 IP 地址和端口号转换为字符串或数字,Linux 操作系统提供了一些函数,例如 inet_ntop
,inet_pton
,htons
,ntohs
等。
- 对于 Unix 域地址族(
AF_UNIX
),使用sockaddr_un
结构体来表示地址,它定义如下:
cpp
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; // 地址族
char sun_path[UNIX_PATH_MAX]; // 路径名
};
其中,sun_family 表示地址族,必须为 AF_UNIX
;sun_path 表示路径名,也就是 Unix 域套接字所对应的文件名。
-
套接字的操作:Linux 操作系统中提供了一些函数来创建和操作套接字,主要有以下几种:
- socket:socket 函数用于创建一个套接字,并返回一个文件描述符,它的原型如下:
cpp#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
其中,domain 参数表示地址族的类型,可以是
AF_INET
,AF_INET6
,AF_UNIX
等;type 参数表示套接字的类型,可以是SOCK_STREAM
,SOCK_DGRAM
,SOCK_RAW
等;protocol 参数表示具体的协议类型,通常为 0,表示使用默认的协议。socket 函数的返回值是一个文件描述符,如果出错则返回 -1,并设置 errno。- bind:bind 函数用于将一个套接字绑定到一个地址上,它的原型如下:
cpp#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中,sockfd 参数是由 socket 函数返回的文件描述符;addr 参数是指向一个 sockaddr 结构体或其子结构体(如 sockaddr_in, sockaddr_in6, sockaddr_un 等)的指针;addrlen 参数是该结构体的大小。bind 函数的返回值是 0,表示成功;如果出错则返回 -1,并设置 errno。
- listen:listen 函数用于将一个流式套接字转换为被动模式(passive mode),也就是说该套接字可以接受其他进程的连接请求,它的原型如下:
cpp#include <sys/types.h> #include <sys/socket.h> int listen(int sockfd, int backlog);
其中,sockfd 参数是由 socket 函数返回的文件描述符;backlog 参数表示等待连接队列(pending connection queue)的最大长度,也就是说该套接字可以等待的最大连接数。listen 函数的返回值是 0,表示成功;如果出错则返回 -1,并设置 errno。
-
accept:accept 函数用于接受一个连接请求,并返回一个新的文件描述符,该文件描述符用于与客户端进行通信,它的原型如下:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
其中,sockfd 参数是由 listen 函数创建的套接字的文件描述符;addr 参数是一个指向 sockaddr 结构体的指针,用于存储客户端的地址信息;addrlen 参数是一个指向整数的指针,用于表示 addr 结构体的大小。accept 函数的返回值是一个新的文件描述符,用于与客户端进行通信;如果出错则返回 -1,并设置 errno。
- connect:connect 函数用于发起一个连接请求,将套接字连接到指定的地址,它的原型如下:
cpp
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其中,sockfd 参数是由 socket 函数创建的套接字的文件描述符;addr 参数是一个指向 sockaddr 结构体的指针,用于指定目标地址;addrlen 参数是 addr 结构体的大小。connect 函数的返回值是 0,表示成功;如果出错则返回 -1,并设置 errno。
- send 和 recv:send 函数用于向已连接的套接字发送数据,recv 函数用于从已连接的套接字接收数据,它们的原型分别如下:
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
其中,sockfd 参数是已连接的套接字的文件描述符;buf 参数是一个指向数据缓冲区的指针;len 参数表示数据的长度;flags 参数用于指定发送或接收数据的选项。send 和 recv 函数的返回值是实际发送或接收的数据长度,如果出错则返回 -1,并设置 errno。
- close:close 函数用于关闭一个套接字,释放相关的资源,它的原型如下:
cpp
#include <unistd.h>
int close(int sockfd);
其中,sockfd 参数是要关闭的套接字的文件描述符。close 函数的返回值是 0,表示成功;如果出错则返回 -1,并设置 errno。
这些是套接字在 Linux 操作系统中的基本操作和数据结构,可以用于实现各种网络通信应用。根据不同的需求和场景,可以选择合适的套接字类型、地址族和操作来构建应用程序。同时,还需要考虑网络安全性、性能优化等方面的问题,以确保应用程序的稳定性和可靠性。