select
是Unix类操作系统中的一个系统调用,主要用于同时监视多个文件描述符(file descriptor, fd),等待其中的一个或多个变为"就绪"状态,以便进行I/O操作(如读、写)。它是实现多路复用I/O(Multiplexed I/O)的经典方法。
#include
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds
:最大文件描述符值 + 1,告诉select
需要检查的文件描述符范围readfds
:读文件描述符集合,用于监视是否有可读事件writefds
:写文件描述符集合,用于监视是否有可写事件exceptfds
:异常文件描述符集合,用于监视异常情况(如OOB数据)timeout
:超时时间,控制select
的等待时间:
NULL
:阻塞,直到至少有一个文件描述符就绪{0, 0}
:非阻塞,立即返回结果EINTR
(被信号中断)单个进程可以监视的文件描述符数量通常受限于FD_SETSIZE
,在Linux头文件中定义:
#define __FD_SETSIZE 1024
select
只能监视最多1024个文件描述符FD_SETSIZE
,但需要重新编译相关的应用程序和库poll
或epoll
,它们不受此限制select
返回的是就绪文件描述符的数量(int
),但不会告诉你哪些fd
发生了事件。因此,应用程序需要手动遍历fd_set
,检查哪些fd
发生了事件:
for (int i = 0; i < nfds; i++) {
if (FD_ISSET(i, &readfds)) {
// 该fd发生了可读事件
}
}
由于select
采用轮询的方式扫描所有文件描述符,这意味着:
fd
很多时,性能较低select
每次调用时,都会从用户空间向内核空间拷贝fd_set
,然后再从内核空间拷贝回用户空间。这带来了以下问题:
FD_SET()
,否则数据会被修改fd
数量较多时,性能开销很大相比之下,epoll
采用事件驱动模型,避免了这种拷贝开销,性能更优。
select
采用水平触发(Level Trigger, LT):
fd
处于就绪状态但未被处理,select
仍然会不断返回该fd
fd
可读,应用程序调用select
,它返回fd
可读select
调用仍然会返回fd
可读相对而言,epoll
支持边缘触发(Edge Trigger, ET),只在状态变化时通知,减少了重复通知,提高了效率。
可移植性好
select
是POSIX标准,几乎所有Unix/Linux以及Windows都支持它epoll
、kqueue
等其他多路复用机制在某些系统上可能不可用超时时间精度更高
select
的timeout
精确到微秒(us)poll
只能精确到毫秒(ms)文件描述符数量受限
FD_SETSIZE
(通常是1024)个fd
poll
和epoll
不受此限制每次调用都要拷贝fd_set
遍历整个fd_set
,效率低
select
不会告诉你哪个fd
发生了事件,应用程序必须遍历所有fd
,时间复杂度O(n)epoll
采用事件回调机制,时间复杂度接近O(1),性能更优方案 | 文件描述符限制 | 拷贝开销 | 遍历方式 | 触发模式 |
---|---|---|---|---|
select |
1024 | 高 | 轮询 O(n) | 水平触发(LT) |
poll |
无限制 | 高 | 轮询 O(n) | 水平触发(LT) |
epoll |
无限制 | 低 | 事件通知 O(1) | 水平触发/边缘触发 |
select
适用于少量fd
的场景,如:
大规模fd
监听,推荐使用epoll
(Linux)或kqueue
(BSD/macOS):
如果你有更大规模的连接需求,建议学习epoll
,它能显著提升性能。
fork()
是Unix/Linux中的一个系统调用,它的作用是创建一个新的进程(子进程)。新进程是调用进程的一个副本,但有自己的进程ID(PID)。父进程和子进程几乎完全相同,但是它们是独立的,可以分别执行不同的任务。
fork()
会复制当前进程,创建一个几乎相同的新进程fork()
的返回值是子进程的PID(大于0)fork()
的返回值是0
fork()
失败,返回-1
,表示进程创建失败#include
#include // 提供fork()函数
#include // 提供pid_t类型
int main() {
pid_t pid = fork(); // 创建子进程
if (pid > 0) {
// 这里是父进程
printf("我是父进程,我的PID是%d,我的子进程PID是%d\n", getpid(), pid);
} else if (pid == 0) {
// 这里是子进程
printf("我是子进程,我的PID是%d,我的父进程PID是%d\n", getpid(), getppid());
} else {
// fork失败
printf("fork失败!\n");
}
return 0;
}
我是父进程,我的PID是1234,我的子进程PID是1235
我是子进程,我的PID是1235,我的父进程PID是1234
解释:
fork()
之后,进程会被复制,因此if
语句后的代码会执行两次,一次在父进程里,一次在子进程里pid > 0
)fork()
返回的0
(pid == 0
)fork()
只是复制进程,父子进程的执行顺序是不确定的,由操作系统调度父进程可以使用wait()
或waitpid()
等待子进程结束,避免子进程变成"僵尸进程"(Zombie Process)。
wait()
使父进程阻塞,直到某个子进程结束-1
#include
#include
#include
#include
int main() {
pid_t pid = fork(); // 创建子进程
if (pid > 0) {
// 父进程
printf("父进程等待子进程结束...\n");
int status;
pid_t child_pid = wait(&status); // 等待子进程
printf("子进程%d结束,退出状态%d\n", child_pid, WEXITSTATUS(status));
} else if (pid == 0) {
// 子进程
printf("我是子进程,我的PID是%d\n", getpid());
sleep(2); // 模拟子进程运行2秒
printf("子进程即将退出...\n");
} else {
printf("fork失败!\n");
}
return 0;
}
父进程等待子进程结束...
我是子进程,我的PID是1235
子进程即将退出...
子进程1235结束,退出状态0
解释:
wait(&status)
让父进程阻塞,直到子进程退出WEXITSTATUS(status)
获取子进程的退出状态exec
系列函数用于在进程中执行一个新程序,替换当前进程映像。exec
执行成功后,当前进程的代码和数据都会被新程序替换,它不会返回。
exec
系列有多个变体:
execl(path, arg0, arg1, ..., NULL)
传递参数列表(l
代表list)execv(path, argv[])
传递参数数组(v
代表vector)execle(path, arg0, ..., NULL, envp)
传递环境变量execve(path, argv[], envp[])
直接使用参数数组和环境变量数组#include
#include
int main() {
printf("当前进程PID: %d\n", getpid());
// 执行ls命令
execl("/bin/ls", "ls", "-l", NULL);
// 只有exec失败才会执行这里
printf("exec执行失败!\n");
return 0;
}
当前进程PID: 1234
total 8
-rw-r--r-- 1 user user 1234 Mar 20 12:00 example.txt
解释:
execl("/bin/ls", "ls", "-l", NULL)
执行ls -l
命令exec
成功后,当前进程的代码被新程序替换,后面的printf()
不会执行exec
失败,则会执行printf("exec执行失败!")
系统调用 | 作用 | 返回值 | 特点 |
---|---|---|---|
fork() |
创建子进程 | 父进程返回子进程PID,子进程返回0,失败返回-1 | 父子进程独立运行 |
wait() |
父进程等待子进程 | 成功返回子进程PID,失败返回-1 | 防止僵尸进程 |
exec() |
执行新程序 | 成功无返回,失败返回-1 | 替换当前进程 |
创建子进程,等待子进程执行exec
,然后回收资源
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程
wait(NULL);
printf("子进程结束,父进程退出。\n");
} else if (pid == 0) {
// 子进程执行ls命令
execl("/bin/ls", "ls", "-l", NULL);
} else {
printf("fork失败!\n");
}
return 0;
}
执行效果
total 8
-rw-r--r-- 1 user user 1234 Mar 20 12:00 example.txt
子进程结束,父进程退出。
下面是一个有趣的算法,用于计算一个整数二进制表示中0的个数。
编写一个函数,计算整数num
的二进制表示中有多少个0。
例如:
num = 25
0001 1001
3
(因为25
的二进制表示中有三个0)#include
#include
// 计算num的二进制表示中0的个数
int CountZeroBit(int num) {
int count = 0; // 初始化计数器,记录0的个数
// 只要num不为0,循环就会继续
while (num + 1) {
count++; // 每次找到一个0位,计数器加1
num |= (num + 1); // 将num的0位转化为1
}
return count; // 返回0的个数
}
int main() {
int value = 25; // 25的二进制表示是 0001 1001
int ret = CountZeroBit(value); // 计算0的个数
printf("%d的二进制位中0的个数为%d\n", value, ret); // 输出结果
system("pause");
return 0;
}
CountZeroBit
函数的核心思想是使用位运算技巧来找出二进制表示中的0位。
while (num + 1) {
count++; // 每次找到一个0位,计数器加1
num |= (num + 1); // 将num的0位转化为1
}
num + 1
:这一步将num
的最低位0变为1。如果num
的最低位是1,它会将num
的最低位1变为0,并且将下一个更低的0位变为1。
例如:
num = 1000
,那么num + 1 = 1001
num = 1110
,那么num + 1 = 1111
num |= (num + 1)
:通过这个操作,我们将num
中的最低0位转化为1。|=
是位或操作符,将num + 1
的结果和num
按位或,结果就是将最低的0变成了1。
假设num = 25
(其二进制表示为0001 1001
),我们要逐步找出其中的0位。
初始化:
num = 25
(0001 1001
)count = 0
(初始时没有找到任何0)第一次循环:
num + 1 = 26
(0001 1010
)num |= 26
:num = 0001 1001 | 0001 1010 = 0001 1011
。这样,num
的第二个0变成了1。count++
,count
变成了1。第二次循环:
num + 1 = 27
(0001 1011
)num |= 27
:num = 0001 1011 | 0001 1100 = 0001 1111
。这样,num
的第三个0变成了1。count++
,count
变成了2。第三次循环:
num + 1 = 28
(0001 1100
)num |= 28
:num = 0001 1100 | 0001 1101 = 0001 1101
。这样,num
的最后一个0变成了1。count++
,count
变成了3。结束条件:
num = 29
(0001 1101
),这时没有更多的0了,循环结束。最终,count
的值是3,表示25的二进制表示中有3个0。
数据结构:
支持下标操作:
operator[]
)来访问和修改元素的值,如果指定的键不存在,operator[]
会默认插入一个新元素。允许修改元素:
map 示例:
在 map 中,每个元素是一个 键值对(key-value)。你可以使用 operator[]
来访问或插入一个键值对。若某个键不存在,operator[]
会插入一个新元素。
#include
#include
int main() {
std::map<std::string, int> age;
// 插入元素
age["Alice"] = 30;
age["Bob"] = 25;
age["Charlie"] = 35;
// 输出map的内容
std::cout << "Map contents:" << std::endl;
for (const auto& pair : age) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
输出:
Map contents:
Alice: 30
Bob: 25
Charlie: 35
set 示例:
在 set 中,每个元素是一个唯一的 key,插入操作会自动排除重复的元素。由于 set 没有 operator[]
,它只提供插入、删除和查找操作。
#include
#include
int main() {
std::set<int> numbers;
// 插入元素
numbers.insert(10);
numbers.insert(20);
numbers.insert(30);
numbers.insert(20); // set中不会插入重复元素
// 输出set的内容
std::cout << "\nSet contents:" << std::endl;
for (const auto& num : numbers) {
std::cout << num << std::endl;
}
return 0;
}
输出:
Set contents:
10
20
30
map的下标运算符:
operator[]
在 map 中不仅用于访问元素,还可以用于插入元素。若所查找的 key 不存在,它会自动插入一个值为默认构造的 mapped_type 的新元素。find()
方法:if (age.find("David") == age.end()) {
std::cout << "David not found" << std::endl;
}
set的特性:
本文详细介绍了Unix/Linux系统编程中几个重要的概念: