epoll_ctl系统调用及示例

epoll_ctl - 控制epoll实例

函数介绍

epoll_ctl系统调用用于控制epoll实例,可以添加、修改或删除要监视的文件描述符。

函数原型

1
2
3
4
#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能

对epoll实例进行控制操作,管理要监视的文件描述符。

参数

  • int epfd: epoll实例的文件描述符

int op: 操作类型

  • EPOLL_CTL_ADD: 添加文件描述符到监视集合

  • EPOLL_CTL_MOD: 修改已监视文件描述符的设置

  • EPOLL_CTL_DEL: 从监视集合中删除文件描述符

int fd: 要控制的文件描述符

struct epoll_event *event: 指向epoll_event结构体的指针

返回值

  • 成功时返回0

  • 失败时返回-1,并设置errno

特殊限制

  • 需要有效的epoll文件描述符

  • 文件描述符必须是有效的

  • 某些操作需要文件描述符已在监视集合中

相似函数

  • epoll_wait(): 等待epoll事件

  • poll(): 传统轮询控制

示例代码

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>

int main() {
int epfd, sockfd;
struct epoll_event ev;

printf("=== Epoll_ctl 函数示例 ===\n");

// 创建epoll实例
epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd == -1) {
perror("epoll_create1 失败");
exit(EXIT_FAILURE);
}
printf("创建epoll实例: %d\n", epfd);

// 创建测试用的socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("创建socket失败");
close(epfd);
exit(EXIT_FAILURE);
}
printf("创建测试socket: %d\n", sockfd);

// 设置socket为非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
printf("设置socket为非阻塞模式\n");

// 示例1: 添加文件描述符到epoll监视集合
printf("\n示例1: 添加文件描述符到epoll\n");

ev.events = EPOLLIN | EPOLLOUT | EPOLLET; // 读事件、写事件、边缘触发
ev.data.fd = sockfd;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
perror("EPOLL_CTL_ADD 失败");
} else {
printf("成功添加socket %d 到epoll监视集合\n", sockfd);
printf("监视事件: EPOLLIN | EPOLLOUT | EPOLLET\n");
}

// 示例2: 修改已监视的文件描述符
printf("\n示例2: 修改已监视的文件描述符\n");

ev.events = EPOLLIN | EPOLLET; // 只监视读事件和边缘触发
ev.data.fd = sockfd;

if (epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev) == -1) {
if (errno == ENOENT) {
printf("修改失败,文件描述符未在监视集合中: %s\n", strerror(errno));
} else {
perror("EPOLL_CTL_MOD 失败");
}
} else {
printf("成功修改socket %d 的监视设置\n", sockfd);
printf("新监视事件: EPOLLIN | EPOLLET\n");
}

// 示例3: 删除文件描述符
printf("\n示例3: 删除文件描述符\n");

if (epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL) == -1) {
if (errno == ENOENT) {
printf("删除失败,文件描述符不在监视集合中: %s\n", strerror(errno));
} else {
perror("EPOLL_CTL_DEL 失败");
}
} else {
printf("成功从epoll监视集合中删除socket %d\n", sockfd);
}

// 示例4: 错误处理演示
printf("\n示例4: 错误处理演示\n");

// 使用无效的epoll文件描述符
if (epoll_ctl(-1, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
if (errno == EBADF) {
printf("无效epoll文件描述符错误处理正确: %s\n", strerror(errno));
}
}

// 使用无效的操作类型
if (epoll_ctl(epfd, 999, sockfd, &ev) == -1) {
if (errno == EINVAL) {
printf("无效操作类型错误处理正确: %s\n", strerror(errno));
}
}

// 使用无效的文件描述符
if (epoll_ctl(epfd, EPOLL_CTL_ADD, -1, &ev) == -1) {
if (errno == EBADF) {
printf("无效文件描述符错误处理正确: %s\n", strerror(errno));
}
}

// 示例5: epoll_event结构说明
printf("\n示例5: epoll_event结构说明\n");
printf("struct epoll_event 结构体:\n");
printf(" uint32_t events; // 事件类型\n");
printf(" epoll_data_t data; // 用户数据\n\n");

printf("常用事件类型:\n");
printf(" EPOLLIN: 可读事件\n");
printf(" EPOLLOUT: 可写事件\n");
printf(" EPOLLPRI: 紧急数据可读\n");
printf(" EPOLLERR: 错误条件\n");
printf(" EPOLLHUP: 挂起事件\n");
printf(" EPOLLET: 边缘触发模式\n");
printf(" EPOLLONESHOT: 一次性事件\n");
printf(" EPOLLRDHUP: 对端关闭连接\n\n");

// 示例6: epoll_data_t联合体说明
printf("epoll_data_t联合体(可存储不同类型的数据):\n");
printf(" void *ptr; // 指针\n");
printf(" int fd; // 文件描述符\n");
printf(" uint32_t u32; // 32位无符号整数\n");
printf(" uint64_t u64; // 64位无符号整数\n\n");

// 演示使用用户数据
printf("示例6: 使用用户数据\n");

struct epoll_event event_with_data;
event_with_data.events = EPOLLIN;
event_with_data.data.fd = sockfd;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event_with_data) == 0) {
printf("使用文件描述符作为用户数据: %d\n", event_with_data.data.fd);

// 删除文件描述符
epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
}

// 示例7: 实际应用场景
printf("\n示例7: 实际应用场景\n");

printf("服务器程序中的典型使用流程:\n");
printf("1. 创建epoll实例\n");
printf("2. 添加监听socket到epoll\n");
printf("3. 在事件循环中:\n");
printf(" a. 调用epoll_wait等待事件\n");
printf(" b. 处理就绪事件\n");
printf(" c. 根据需要添加/修改/删除监视的文件描述符\n\n");

printf("常见操作模式:\n");
printf("监听socket: EPOLLIN | EPOLLET\n");
printf("客户端socket: EPOLLIN | EPOLLOUT | EPOLLET\n");
printf("一次性事件: EPOLLIN | EPOLLONESHOT\n\n");

// 清理资源
close(sockfd);
close(epfd);

printf("总结:\n");
printf("epoll_ctl是管理epoll监视集合的核心函数\n");
printf("支持添加、修改、删除三种基本操作\n");
printf("正确使用事件类型和用户数据很重要\n");
printf("需要妥善处理各种错误情况\n");

return 0;
}

https://www.calcguide.tech/2025/08/09/epoll_ctl系统调用及示例/

eventfd系统调用及示例

eventfd - 创建事件文件描述符

函数介绍

eventfd系统调用用于创建一个事件文件描述符,用于用户空间程序之间的事件通知。它提供了一个简单的计数器机制,可以用于线程间或进程间的同步。

函数原型

1
2
3
4
5
6
7
#include <sys/eventfd.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>

int eventfd(unsigned int initval, int flags);

功能

创建一个事件文件描述符,内部维护一个64位无符号整数计数器,用于事件通知和同步。

参数

  • unsigned int initval: 计数器的初始值

int flags: 控制标志

  • 0: 基本模式

  • EFD_CLOEXEC: 设置执行时关闭标志

  • EFD_NONBLOCK: 设置非阻塞模式

  • EFD_SEMAPHORE: 信号量模式(每次读取递减1而不是重置为0)

返回值

  • 成功时返回事件文件描述符(非负整数)

  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.6.22以上内核支持

  • 计数器值有最大限制(0xfffffffffffffffeULL)

相似函数

  • eventfd2(): 现代版本,更好的标志支持

  • pipe(): 管道机制

  • signalfd(): 信号文件描述符

示例代码

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <sys/syscall.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>

// 系统调用包装(如果glibc不支持)
static int eventfd_wrapper(unsigned int initval, int flags) {
return syscall(__NR_eventfd2, initval, flags);
}

// 线程函数
void* thread_function(void* arg) {
int efd = *(int*)arg;
uint64_t value = 1;

printf(" 子线程: 准备发送事件通知\n");

// 发送事件通知
if (write(efd, &value, sizeof(value)) != sizeof(value)) {
perror(" 子线程: 写入eventfd失败");
} else {
printf(" 子线程: 成功发送事件通知\n");
}

return NULL;
}

int main() {
int efd;
uint64_t value;
pthread_t thread;

printf("=== Eventfd 函数示例 ===\n");

// 示例1: 基本使用
printf("\n示例1: 基本使用\n");

efd = eventfd_wrapper(0, 0);
if (efd == -1) {
perror("eventfd 创建失败");
exit(EXIT_FAILURE);
}
printf("成功创建eventfd,文件描述符: %d\n", efd);

// 检查文件描述符属性
int flags = fcntl(efd, F_GETFD);
if (flags != -1) {
printf("eventfd文件描述符验证成功\n");
}

// 关闭eventfd
close(efd);
printf("关闭eventfd\n");

// 示例2: 基本的事件通知
printf("\n示例2: 基本的事件通知\n");

efd = eventfd_wrapper(0, 0);
if (efd == -1) {
perror("eventfd 创建失败");
exit(EXIT_FAILURE);
}
printf("创建eventfd: %d\n", efd);

// 启动线程发送事件
if (pthread_create(&thread, NULL, thread_function, &efd) != 0) {
perror("创建线程失败");
close(efd);
exit(EXIT_FAILURE);
}

printf("主线程: 等待事件通知...\n");

// 等待事件通知
ssize_t bytes_read = read(efd, &value, sizeof(value));
if (bytes_read == sizeof(value)) {
printf("主线程: 收到事件通知,计数器值: %lu\n", value);
} else {
perror("主线程: 读取eventfd失败");
}

// 等待线程结束
pthread_join(thread, NULL);

close(efd);

// 示例3: 使用标志位
printf("\n示例3: 使用标志位\n");

// 使用EFD_CLOEXEC标志
efd = eventfd_wrapper(0, EFD_CLOEXEC);
if (efd != -1) {
printf("创建带EFD_CLOEXEC标志的eventfd: %d\n", efd);

// 验证标志是否设置
flags = fcntl(efd, F_GETFD);
if (flags != -1 && (flags & FD_CLOEXEC)) {
printf("EFD_CLOEXEC标志已正确设置\n");
}
close(efd);
}

// 使用EFD_NONBLOCK标志
efd = eventfd_wrapper(0, EFD_NONBLOCK);
if (efd != -1) {
printf("创建带EFD_NONBLOCK标志的eventfd: %d\n", efd);

// 尝试非阻塞读取(应该失败)
bytes_read = read(efd, &value, sizeof(value));
if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("非阻塞读取正确返回EAGAIN: %s\n", strerror(errno));
}
}
close(efd);
}

// 示例4: 信号量模式
printf("\n示例4: 信号量模式\n");

efd = eventfd_wrapper(3, EFD_SEMAPHORE);
if (efd != -1) {
printf("创建信号量模式eventfd,初始值: 3\n");

// 多次读取,每次递减1
for (int i = 0; i < 5; i++) {
bytes_read = read(efd, &value, sizeof(value));
if (bytes_read == sizeof(value)) {
printf("第%d次读取,获得值: %lu\n", i+1, value);
} else {
if (errno == EAGAIN) {
printf("第%d次读取,无可用事件: %s\n", i+1, strerror(errno));
break;
}
}
}

close(efd);
}

// 示例5: 错误处理演示
printf("\n示例5: 错误处理演示\n");

// 使用无效的初始值(虽然eventfd允许大值,但有上限)
efd = eventfd_wrapper(0xffffffff, 0);
if (efd != -1) {
printf("使用大初始值创建eventfd成功: %d\n", efd);
close(efd);
}

// 尝试写入无效值
efd = eventfd_wrapper(0, 0);
if (efd != -1) {
uint64_t invalid_value = 0xffffffffffffffffULL; // 最大值
ssize_t result = write(efd, &invalid_value, sizeof(invalid_value));
if (result == -1) {
printf("写入最大值失败: %s\n", strerror(errno));
} else {
printf("写入最大值成功\n");
}
close(efd);
}

// 示例6: 计数器溢出处理
printf("\n示例6: 计数器溢出处理\n");

efd = eventfd_wrapper(0, 0);
if (efd != -1) {
// 写入接近最大值的数据
uint64_t large_value = 0xfffffffffffffffeULL; // 接近最大值
if (write(efd, &large_value, sizeof(large_value)) == sizeof(large_value)) {
printf("写入大值成功\n");

// 再次写入会导致溢出
uint64_t add_value = 2;
ssize_t result = write(efd, &add_value, sizeof(add_value));
if (result == -1) {
if (errno == EAGAIN) {
printf("计数器溢出,写入失败: %s\n", strerror(errno));
}
}
}
close(efd);
}

// 示例7: 实际应用场景
printf("\n示例7: 实际应用场景\n");

printf("eventfd的典型应用场景:\n");
printf("1. 线程池任务通知\n");
printf("2. 异步I/O完成通知\n");
printf("3. 事件驱动编程\n");
printf("4. 进程间简单通信\n");
printf("5. 与epoll配合使用\n\n");

// 演示与epoll配合使用
printf("与epoll配合使用的示例:\n");
printf("int epfd = epoll_create1(EPOLL_CLOEXEC);\n");
printf("int efd = eventfd(0, EFD_CLOEXEC);\n");
printf("struct epoll_event ev;\n");
printf("ev.events = EPOLLIN;\n");
printf("ev.data.fd = efd;\n");
printf("epoll_ctl(epfd, EPOLL_CTL_ADD, efd, &ev);\n");
printf("// 在其他线程中: write(efd, &value, sizeof(value));\n");
printf("// 在事件循环中: epoll_wait(epfd, events, maxevents, timeout);\n\n");

// 示例8: 性能优势
printf("示例8: 性能优势\n");
printf("eventfd相比传统机制的优势:\n");
printf("1. 更少的系统调用\n");
printf("2. 更小的内存占用\n");
printf("3. 更快的通知速度\n");
printf("4. 更简单的API\n");
printf("5. 更好的可扩展性\n\n");

printf("与pipe的对比:\n");
printf("pipe: 需要两个文件描述符,缓冲区较大\n");
printf("eventfd: 只需要一个文件描述符,固定8字节计数器\n\n");

printf("总结:\n");
printf("eventfd是Linux提供的轻量级事件通知机制\n");
printf("适用于简单的同步和通知场景\n");
printf("支持多种模式和标志位\n");
printf("与epoll等机制配合使用效果更佳\n");
printf("是现代Linux编程的重要工具\n");

return 0;
}

  1. eventfd2 - 创建事件文件描述符(扩展版)

函数介绍

eventfd2是eventfd的扩展版本,提供了更好的标志位支持和错误处理。它是现代Linux系统推荐使用的eventfd创建函数。

函数原型

1
2
3
4
#include <sys/eventfd.h>

int eventfd2(unsigned int initval, int flags);

功能

创建一个事件文件描述符,功能与eventfd相同但接口更现代。

参数

  • unsigned int initval: 计数器的初始值

  • int flags: 控制标志(支持更多标志位)

返回值

  • 成功时返回事件文件描述符

  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.6.27以上内核支持

  • 某些旧系统可能不支持

相似函数

  • eventfd(): 基础版本

  • pipe(): 管道机制

示例代码

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/eventfd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <pthread.h>

int main() {
int efd1, efd2;

printf("=== Eventfd2 函数示例 ===\n");

// 示例1: 基本使用对比
printf("\n示例1: 基本使用对比\n");

// 使用eventfd
efd1 = eventfd(0, 0);
if (efd1 != -1) {
printf("eventfd创建成功: %d\n", efd1);
close(efd1);
} else {
printf("eventfd创建失败: %s\n", strerror(errno));
}

// 使用eventfd2
efd2 = eventfd2(0, 0);
if (efd2 != -1) {
printf("eventfd2创建成功: %d\n", efd2);
close(efd2);
} else {
printf("eventfd2创建失败: %s\n", strerror(errno));
printf("说明: 系统可能不支持eventfd2\n");
}

// 示例2: 标志位支持
printf("\n示例2: 标志位支持\n");

// EFD_CLOEXEC标志
efd2 = eventfd2(0, EFD_CLOEXEC);
if (efd2 != -1) {
printf("使用EFD_CLOEXEC标志创建成功: %d\n", efd2);

// 验证标志设置
int flags = fcntl(efd2, F_GETFD);
if (flags != -1 && (flags & FD_CLOEXEC)) {
printf("EFD_CLOEXEC标志验证成功\n");
}
close(efd2);
}

// EFD_NONBLOCK标志
efd2 = eventfd2(0, EFD_NONBLOCK);
if (efd2 != -1) {
printf("使用EFD_NONBLOCK标志创建成功: %d\n", efd2);

// 测试非阻塞特性
uint64_t value;
ssize_t result = read(efd2, &value, sizeof(value));
if (result == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("非阻塞读取正确返回EAGAIN\n");
}
}
close(efd2);
}

// EFD_SEMAPHORE标志
efd2 = eventfd2(5, EFD_SEMAPHORE);
if (efd2 != -1) {
printf("使用EFD_SEMAPHORE标志创建成功,初始值: 5\n");

// 测试信号量模式
for (int i = 0; i < 7; i++) {
uint64_t read_value;
ssize_t result = read(efd2, &read_value, sizeof(read_value));
if (result == sizeof(read_value)) {
printf("第%d次读取成功,值: %lu\n", i+1, read_value);
} else {
if (errno == EAGAIN) {
printf("第%d次读取失败,无可用资源: %s\n", i+1, strerror(errno));
break;
}
}
}
close(efd2);
}

// 示例3: 组合标志
printf("\n示例3: 组合标志\n");

// 组合多个标志
efd2 = eventfd2(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (efd2 != -1) {
printf("组合标志创建成功: %d\n", efd2);

// 验证所有标志
int flags = fcntl(efd2, F_GETFD);
if (flags != -1) {
if (flags & FD_CLOEXEC) {
printf("EFD_CLOEXEC标志已设置\n");
}
}

// 测试非阻塞特性
uint64_t value;
if (read(efd2, &value, sizeof(value)) == -1) {
if (errno == EAGAIN) {
printf("EFD_NONBLOCK标志生效\n");
}
}

close(efd2);
}

// 示例4: 错误处理
printf("\n示例4: 错误处理\n");

// 使用无效标志
efd2 = eventfd2(0, 0x1000); // 无效标志
if (efd2 == -1) {
if (errno == EINVAL) {
printf("无效标志错误处理正确: %s\n", strerror(errno));
}
}

// 在不支持的系统上
printf("在不支持eventfd2的系统上会返回ENOSYS错误\n");

// 示例5: 与eventfd的差异
printf("\n示例5: 与eventfd的差异\n");

printf("eventfd vs eventfd2:\n");
printf("eventfd:\n");
printf(" - 较老的接口\n");
printf(" - 标志位支持有限\n");
printf(" - 在所有支持eventfd的系统上可用\n\n");

printf("eventfd2:\n");
printf(" - 现代接口\n");
printf(" - 更好的标志位支持\n");
printf(" - 原子性设置标志\n");
printf(" - 需要Linux 2.6.27+\n\n");

// 示例6: 实际应用推荐
printf("示例6: 实际应用推荐\n");

printf("现代应用推荐使用模式:\n");
printf("#ifdef EFD_CLOEXEC\n");
printf(" int efd = eventfd2(0, EFD_CLOEXEC | EFD_NONBLOCK);\n");
printf("#else\n");
printf(" int efd = eventfd(0, 0);\n");
printf(" fcntl(efd, F_SETFD, FD_CLOEXEC);\n");
printf(" fcntl(efd, F_SETFL, fcntl(efd, F_GETFL) | O_NONBLOCK);\n");
printf("#endif\n\n");

// 示例7: 兼容性处理
printf("示例7: 兼容性处理\n");

// 兼容性包装函数
printf("兼容性处理示例:\n");
printf("int create_eventfd(unsigned int initval, int flags) {\n");
printf("#ifdef __NR_eventfd2\n");
printf(" int fd = eventfd2(initval, flags);\n");
printf(" if (fd >= 0 || errno != ENOSYS)\n");
printf(" return fd;\n");
printf("#endif\n");
printf(" // 回退到eventfd\n");
printf(" fd = eventfd(initval, 0);\n");
printf(" if (fd >= 0) {\n");
printf(" // 手动设置标志\n");
printf(" if (flags & EFD_CLOEXEC)\n");
printf(" fcntl(fd, F_SETFD, FD_CLOEXEC);\n");
printf(" if (flags & EFD_NONBLOCK)\n");
printf(" fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);\n");
printf(" }\n");
printf(" return fd;\n");
printf("}\n\n");

// 示例8: 性能考虑
printf("示例8: 性能考虑\n");
printf("eventfd2性能优势:\n");
printf("1. 原子性标志设置,避免竞态条件\n");
printf("2. 减少系统调用次数\n");
printf("3. 更好的错误处理\n");
printf("4. 现代内核优化\n\n");

printf("使用建议:\n");
printf("1. 优先使用eventfd2\n");
printf("2. 提供eventfd回退方案\n");
printf("3. 合理使用标志位\n");
printf("4. 注意资源清理\n\n");

printf("总结:\n");
printf("eventfd2是eventfd的现代替代品\n");
printf("提供了更好的标志位支持\n");
printf("在支持的系统上应优先使用\n");
printf("需要考虑向后兼容性\n");
printf("是构建高性能应用的重要工具\n");

return 0;
}

execveat系统调用及示例

execveat系统调用及示例

探索execveat系统调用的深入解析与实用示例,掌握Linux编程技巧。立即学习,提升你的代码能力!

1. 函数介绍

execveat 是一个 Linux 系统调用(内核版本 >= 3.19),它是 execve 函数族的一员。它的核心功能是在当前进程中执行一个新的程序,从而替换当前进程的镜像(代码、数据、堆栈等)。

简单来说,execveat 就像用一个新灵魂替换旧灵魂:

  • 你的身体(进程)还在,但里面的思想、记忆、行为(程序代码和数据)被完全替换成另一个人(新程序)的。

  • 旧程序的所有状态(局部变量、堆栈)都消失了。

  • 新程序从它的 main 函数开始执行。

execveat 相比于 execve 的独特之处在于它引入了目录文件描述符(dirfd)和路径解析标志(flags),使得程序执行可以相对于一个已打开的目录进行,或者直接执行一个已打开的文件描述符所指向的文件。这提供了更灵活和安全的程序执行方式,尤其是在处理复杂路径或受限环境(如容器)时。

2. 函数原型

1
2
3
4
5
#include <unistd.h> // 必需

int execveat(int dirfd, const char *pathname,
char *const argv&#91;], char *const envp&#91;], int flags);

3. 功能

  • 执行新程序: 终止调用进程的当前程序,并使用由 pathname(结合 dirfd 和 flags)指定的可执行文件来替换当前进程的内存镜像。

  • 传递参数和环境: 将新的命令行参数 (argv) 和环境变量 (envp) 传递给新程序。

  • 灵活的路径解析: 通过 dirfd 和 flags 参数,提供了比 execve 更灵活的路径解析方式。

4. 参数

int dirfd: 一个目录文件描述符,用作解析 pathname 的起始点。

  • 如果 pathname 是相对路径(例如 “subdir/myprogram”),则相对于 dirfd 指向的目录进行查找。

  • 如果 pathname 是绝对路径(例如 “/usr/bin/ls”),则 dirfd 被忽略。

  • 可以传入特殊的值 AT_FDCWD,表示使用当前工作目录作为起始点(此时行为类似于 execve)。

const char *pathname: 指向要执行的可执行文件的路径名。这个路径名会根据 dirfd 和 flags 进行解析。

char *const argv[]: 一个字符串数组(向量),用于传递给新程序的命令行参数。

  • 数组的每个元素都是一个指向以空字符 (\0) 结尾的字符串的指针。

  • 数组必须以 NULL 指针结束。

  • argv[0] 通常是程序的名字(惯例,但不是强制)。

  • 例如:char *argv[] = {“myprogram”, “–verbose”, “input.txt”, NULL};

char *const envp[]: 一个字符串数组(向量),用于设置新程序的环境变量。

  • 数组的每个元素都是一个形如 “NAME=VALUE” 的字符串。

  • 数组必须以 NULL 指针结束。

  • 例如:char *envp[] = {“PATH=/usr/bin:/bin”, “HOME=/home/user”, NULL};

  • 可以使用全局变量 environ 来传递当前进程的环境。

int flags: 控制路径解析行为的标志位。可以是以下值的按位或组合:

  • 0: 默认行为。pathname 被当作普通路径名处理,相对于 dirfd 解析。

  • AT_EMPTY_PATH: 如果 pathname 是一个空字符串 (“”),则 execveat 会尝试执行 dirfd 本身所引用的文件。dirfd 必须是一个有效的、指向可执行文件的文件描述符。

  • AT_SYMLINK_NOFOLLOW: 如果 pathname 解析过程中遇到符号链接(symbolic link),则不跟随该链接,而是尝试执行符号链接文件本身。注意:并非所有文件系统都支持执行符号链接文件本身,通常会失败。

5. 返回值

  • 成功时: 永不返回。因为 execveat 成功执行后,当前进程的代码和数据已经被新程序完全替换,从新程序的 main 函数开始执行。

  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EACCES 权限不足,ENOENT 文件未找到,EINVAL 参数无效,ENOMEM 内存不足,ELIBBAD 动态链接库损坏等)。

重要: 只有在 execveat 调用失败时,它才会返回 -1。如果成功,控制流就不会回到调用 execveat 的代码处。

6. 相似函数,或关联函数

  • execve: 最基础的 exec 函数,功能与 execveat 相同,但不支持 dirfd 和 flags 参数。

  • execl, execlp, execle, execv, execvp: execve 的各种包装函数,提供了不同的参数传递方式(如使用可变参数列表 … 或自动搜索 PATH 环境变量)。

  • fork: 通常与 execveat/execve 配合使用。fork 创建子进程,然后在子进程中调用 execveat/execve 来执行新程序。

  • system: 一个更高级的库函数,内部通过 fork + execve (/bin/sh -c command) 来执行 shell 命令。

  • posix_spawn: POSIX 标准定义的、更现代和可移植的创建和执行子进程的方式,功能强大且避免了 fork 的开销(在某些系统上)。

7. 示例代码

示例 1:基本的 execveat 使用

这个例子演示了如何使用 execveat 来执行一个简单的程序(/bin/echo)。

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
// execveat_basic.c
#define _GNU_SOURCE // For AT_FDCWD
#include <unistd.h> // execveat
#include <fcntl.h> // open, O_RDONLY, AT_FDCWD
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
#include <errno.h> // errno

int main() {
// 1. 准备 execveat 的参数
const char *pathname = "/bin/echo";
char *const argv&#91;] = {"echo", "Hello,", "from", "execveat!", NULL};
// 使用当前进程的环境变量
extern char **environ;
char *const envp&#91;] = {NULL}; // 或者直接传递 environ

printf("About to execute '/bin/echo' using execveat...\n");

// 2. 调用 execveat
// dirfd = AT_FDCWD: 使用当前工作目录解析绝对路径
// pathname = "/bin/echo": 要执行的程序
// argv: 传递给新程序的参数
// environ: 传递给新程序的环境变量
// flags = 0: 默认行为
if (execveat(AT_FDCWD, pathname, argv, environ, 0) == -1) {
// 3. 如果 execveat 返回,说明执行失败
perror("execveat failed");
exit(EXIT_FAILURE);
}

// 4. 这行代码永远不会被执行,因为 execveat 成功后不会返回
printf("This line will never be printed if execveat succeeds.\n");

return 0; // 这行也不会被执行
}

代码解释:

定义要执行的程序路径 pathname (“/bin/echo”)。

准备传递给新程序的命令行参数 argv 数组。argv[0] 通常是程序名 “echo”,后面跟着要传递的参数 “Hello,”, “from”, “execveat!”。非常重要:数组必须以 NULL 结尾。

准备环境变量 envp。这里为了简化,传递一个只包含 NULL 的数组,表示不传递任何环境变量。实际应用中通常传递 extern char **environ 来继承当前进程的环境。

调用 execveat(AT_FDCWD, pathname, argv, environ, 0)。

  • AT_FDCWD: 使用当前工作目录作为路径解析起点。

  • pathname: 要执行的程序的绝对路径。

  • argv: 参数向量。

  • environ: 环境变量向量。

  • 0: 默认标志。

关键: 检查 execveat 的返回值。如果它返回 -1,说明执行失败,打印错误信息并退出。

如果 execveat 成功,当前进程的代码被 /bin/echo 程序替换,从 echo 的 main 函数开始执行,打印 “Hello, from execveat!”,然后 echo 程序退出,整个进程也随之结束。

main 函数中 execveat 之后的代码(包括 return 0;)永远不会被执行。

示例 2:使用 execveat 和 AT_EMPTY_PATH 执行已打开的文件

这个例子演示了如何使用 AT_EMPTY_PATH 标志来执行一个已经通过文件描述符打开的可执行文件。

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
// execveat_empty_path.c
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
const char *binary_path = "/bin/ls"; // 我们将执行 ls 命令
int binary_fd;
char *const argv&#91;] = {"ls", "-l", "/tmp", NULL}; // ls -l /tmp
extern char **environ;

printf("Opening executable file '%s'...\n", binary_path);

// 1. 打开可执行文件,获取文件描述符
binary_fd = open(binary_path, O_RDONLY);
if (binary_fd == -1) {
perror("open executable file");
exit(EXIT_FAILURE);
}
printf("Executable file opened successfully. File descriptor: %d\n", binary_fd);

// 2. 准备 execveat 的参数
// 注意:pathname 是空字符串 ""
const char *pathname = "";

printf("About to execute using execveat with AT_EMPTY_PATH...\n");
printf("Command will be: ls -l /tmp\n");

// 3. 调用 execveat
// dirfd = binary_fd: 指向已打开的可执行文件的文件描述符
// pathname = "": 空字符串
// argv: 传递给 ls 的参数
// environ: 环境变量
// flags = AT_EMPTY_PATH: 告诉 execveat 执行 dirfd 指向的文件
if (execveat(binary_fd, pathname, argv, environ, AT_EMPTY_PATH) == -1) {
// 4. 如果 execveat 返回,说明执行失败
perror("execveat with AT_EMPTY_PATH failed");
// 可能的原因:内核版本 < 3.19, binary_fd 无效, 文件不可执行等
close(binary_fd); // 关闭文件描述符
exit(EXIT_FAILURE);
}

// 5. 这行代码永远不会被执行
printf("This line will never be printed if execveat succeeds.\n");

// close(binary_fd); // 这行也不会被执行,因为 execveat 成功后进程已替换
return 0;
}

代码解释:

定义要执行的二进制文件路径 binary_path (“/bin/ls”)。

使用 open(binary_path, O_RDONLY) 以只读方式打开该可执行文件,并获得文件描述符 binary_fd。

准备 execveat 的参数:

  • pathname 被设置为空字符串 “”。

  • argv 设置为执行 ls -l /tmp 所需的参数。

  • envp 传递 environ。

关键步骤: 调用 execveat(binary_fd, “”, argv, environ, AT_EMPTY_PATH)。

  • binary_fd: 之前打开的可执行文件的文件描述符。

  • “”: 空字符串 pathname。

  • AT_EMPTY_PATH: 这个标志告诉 execveat:忽略 pathname,直接执行 dirfd(即 binary_fd)所指向的文件。

检查返回值。如果成功,当前进程被 /bin/ls 程序替换,并执行 ls -l /tmp 命令。

如果失败,打印错误信息,关闭 binary_fd 并退出。

示例 3:execveat 与 fork 结合使用

这个例子演示了 execveat 如何与 fork 结合,创建一个子进程并在其中执行新程序。

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
// execveat_with_fork.c
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/wait.h> // waitpid
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
pid_t pid;
int status;
const char *pathname = "/bin/date"; // 执行 date 命令
char *const argv&#91;] = {"date", "+%Y-%m-%d %H:%M:%S", NULL};
extern char **environ;

printf("Parent process (PID: %d) is about to fork.\n", getpid());

// 1. 创建子进程
pid = fork();
if (pid == -1) {
// fork 失败
perror("fork");
exit(EXIT_FAILURE);
}

if (pid == 0) {
// --- 子进程 ---
printf("Child process (PID: %d) created.\n", getpid());

// 2. 在子进程中调用 execveat 执行新程序
printf("Child process executing '/bin/date'...\n");

if (execveat(AT_FDCWD, pathname, argv, environ, 0) == -1) {
// 3. execveat 失败
perror("execveat in child failed");
_exit(EXIT_FAILURE); // 子进程使用 _exit 退出
}

// 4. 这行代码在子进程中永远不会被执行
printf("This line in child will never be printed.\n");

} else {
// --- 父进程 ---
printf("Parent process (PID: %d) forked child (PID: %d).\n", getpid(), pid);

// 5. 父进程等待子进程结束
printf("Parent process waiting for child to finish...\n");
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}

// 6. 检查子进程退出状态
if (WIFEXITED(status)) {
printf("Child process (PID: %d) exited normally with status %d.\n",
pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child process (PID: %d) was killed by signal %d.\n",
pid, WTERMSIG(status));
} else {
printf("Child process (PID: %d) exited abnormally.\n", pid);
}

printf("Parent process (PID: %d) finished.\n", getpid());
}

return 0;
}

代码解释:

定义要执行的程序 “/bin/date” 及其参数。

调用 fork() 创建子进程。

fork 返回后:
在子进程 (pid == 0):

  • 调用 execveat(AT_FDCWD, pathname, argv, environ, 0) 执行 /bin/date 命令。

  • 如果 execveat 成功,子进程的代码被替换,date 命令开始执行并打印当前日期时间。

  • date 命令执行完毕后,子进程退出。

  • 如果 execveat 失败,打印错误信息并使用 _exit(EXIT_FAILURE) 退出(在子进程中推荐使用 _exit 而非 exit)。

在父进程 (pid > 0):

  • 打印子进程 PID。

  • 调用 waitpid(pid, &status, 0) 等待子进程结束。

  • waitpid 返回后,检查子进程的退出状态 status。

  • 使用 WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG 等宏来判断子进程是正常退出还是被信号终止,并获取退出码或信号号。

  • 父进程打印相关信息并退出。

重要提示与注意事项:

永不返回: execveat 成功时永远不会返回到调用者。这是 exec 系列函数的根本特性。

失败处理: 必须检查 execveat 的返回值。如果返回 -1,表示执行失败,需要进行错误处理(如打印错误信息、退出等)。

argv 和 envp 必须以 NULL 结尾: 这是 execve 系列函数的严格要求。

AT_EMPTY_PATH: 这是 execveat 相比 execve 的关键新增功能,允许通过文件描述符直接执行文件,这在容器技术和安全沙箱中非常有用。

AT_SYMLINK_NOFOLLOW: 尝试执行符号链接本身,但支持有限。

dirfd 的使用: 提供了更灵活的路径解析能力,尤其是在受限或复杂目录结构中。

与 fork 结合: 这是创建新进程并执行新程序的经典模式(fork-and-exec 模式)。

内核版本: 需要 Linux 内核 3.19 或更高版本。

权限: 执行文件需要有可执行权限 (x)。

environ: 使用 extern char **environ; 可以方便地传递当前进程的环境变量给新程序。

总结:

execveat 是 execve 的强大扩展,通过引入 dirfd 和 flags 参数,提供了更灵活、更安全的程序执行方式。它允许程序基于已打开的目录或文件描述符来定位和执行可执行文件,这在现代 Linux 系统编程,特别是容器和沙箱技术中具有重要意义。理解其参数和与 fork 的结合使用是掌握 Linux 进程控制的基础。

https://www.calcguide.tech/2025/08/09/execveat系统调用及示例/

execve系统调用及示例

linux命名空间系统调用及示例

execve系统调用及示例

好的,我们继续按照您的列表顺序,介绍下一个函数是 execve。

1. 函数介绍

execve 是 Linux 系统编程中一组被称为 exec 函数族的核心成员之一。它的功能是用一个新的程序镜像(program image)

你可以把 execve 想象成彻底的身份转变:

  • 你是一个人(当前运行的进程)。

  • 你决定彻底改变自己,变成另一个人(一个全新的程序)。

  • 你喝下了一瓶神奇药水(调用 execve)。

  • 瞬间,你的外表、记忆、技能、思维方式全部变成了那个人的(新的程序代码、数据、堆栈)。

  • 你不再是原来的你,而是完全变成了新程序的实例。

  • 你的身份(PID)可能保持不变,但你的“灵魂”(程序代码)已经彻底替换。

execve(以及整个 exec 函数族)是实现程序执行的根本机制。当你在 shell 中输入命令(如 ls, grep, gcc)并按回车时,shell 实际上是通过 fork 创建一个子进程,然后在子进程中调用 execve 来运行你指定的程序。

2. 函数原型

1
2
3
4
#include <unistd.h> // 必需

int execve(const char *pathname, char *const argv&#91;], char *const envp&#91;]);

3. 功能

  • 替换进程镜像: 用由 pathname 指定的新程序的镜像完全替换调用 execve 的当前进程的镜像。

  • 加载新程序: 内核会加载 pathname 指定的可执行文件。

  • 初始化新程序: 内核会为新程序分配内存,将程序代码和数据加载到内存中,初始化堆栈,并设置程序计数器(PC)指向程序的入口点(通常是 main 函数)。

  • 传递参数和环境: 将 argv 指定的命令行参数和 envp 指定的环境变量传递给新程序。

  • 开始执行: 从新程序的入口点开始执行新程序。

4. 参数

const char *pathname: 这是一个指向以空字符结尾的字符串的指针,该字符串包含了要执行的新程序的路径名。

  • 这个路径名必须指向一个有效的、可执行的文件。

  • 它可以是绝对路径(如 /bin/ls)或相对路径(如 ./my_program)。

char *const argv[]: 这是一个指针数组,数组中的每个元素都是一个指向以空字符结尾的字符串的指针。这些字符串构成了传递给新程序的命令行参数。

  • 惯例: argv[0] 通常是程序的名字(或调用它的名字)。

  • 结尾: 数组的最后一个元素必须是 NULL,以标记参数列表的结束。

  • 例如:char *args[] = { “ls”, “-l”, “/home”, NULL };

char *const envp[]: 这也是一个指针数组,数组中的每个元素都是一个指向以空字符结尾的字符串的指针。这些字符串定义了新程序的环境变量。

  • 格式: 每个字符串的格式通常是 NAME=VALUE。

  • 结尾: 数组的最后一个元素必须是 NULL,以标记环境变量列表的结束。

  • 例如:char *env_vars[] = { “HOME=/home/user”, “PATH=/usr/bin:/bin”, NULL };

  • 获取当前环境: 在 C 程序中,可以通过全局变量 extern char **environ; 来访问当前进程的环境变量列表。如果你想让新程序继承当前进程的所有环境变量,可以将 environ 作为 envp 参数传递。

5. 返回值

  • 成功时: execve 永远不会返回。如果调用成功,当前进程的镜像就被新程序完全替代,执行从新程序的入口点开始。

  • 失败时: 如果 execve 调用失败(例如,文件不存在、权限不足、文件不是有效的可执行格式等),它会返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EACCES 权限不足,ENOENT 文件不存在,EINVAL 文件格式无效等)。

关键理解点: execve 的成功调用是**“不归之路”**。一旦成功,调用 execve 的代码就不再存在了。

6. 相似函数,或关联函数

execve 是 exec 函数族中最底层、最通用的函数。其他 exec 函数都是基于 execve 或与其紧密相关的变体:

execl: int execl(const char *path, const char *arg, …, (char *)NULL);

  • 参数以列表(list)形式传递,而不是数组。

  • 最后一个参数必须是 (char *)NULL。

  • 使用当前进程的 environ 作为环境。

execlp: int execlp(const char *file, const char *arg, …, (char *)NULL);

  • 与 execl 类似,但会在 PATH 环境变量指定的目录中搜索可执行文件。

execle: int execle(const char *path, const char *arg, …, (char *)NULL, char *const envp[]);

  • 与 execl 类似,但允许指定自定义的环境变量数组 envp。

execv: int execv(const char *path, char *const argv[]);

  • 参数以数组(vector)形式传递。

  • 使用当前进程的 environ 作为环境。

execvp: int execvp(const char *file, char *const argv[]);

  • 与 execv 类似,但会在 PATH 环境变量指定的目录中搜索可执行文件。

execvpe: int execvpe(const char *file, char *const argv[], char *const envp[]); (GNU 扩展)

  • 与 execvp 类似,但允许指定自定义的环境变量数组 envp。

7. 示例代码

示例 1:使用 execve 执行 /bin/ls

这个例子演示了如何使用最底层的 execve 函数来执行 /bin/ls -l /tmp 命令。

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
// execve_ls.c
#include <unistd.h> // execve
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit

// 全局变量 environ,指向当前进程的环境变量数组
extern char **environ;

int main() {
// 1. 定义要执行的程序路径
char *pathname = "/bin/ls";

// 2. 定义命令行参数数组 (argv)
// 注意: argv&#91;0] 通常是程序名,数组必须以 NULL 结尾
char *argv&#91;] = { "ls", "-l", "/tmp", NULL };

// 3. 定义环境变量数组 (envp)
// 为了简化,我们让新程序继承当前进程的所有环境变量
// 通过传递全局变量 environ
char **envp = environ; // 或者可以构造一个自定义的 envp 数组

printf("About to execute: %s %s %s\n", argv&#91;0], argv&#91;1], argv&#91;2]);

// --- 关键: 调用 execve ---
// 如果成功,execve 永远不会返回
execve(pathname, argv, envp);

// --- 如果代码执行到这里,说明 execve 失败了 ---
perror("execve failed");
// 打印错误信息后,程序继续执行下面的代码

printf("This line will only be printed if execve fails.\n");
exit(EXIT_FAILURE); // 因此,如果 execve 失败,应该显式退出
}

代码解释:

定义要执行的程序的完整路径 pathname (“/bin/ls”)。

定义命令行参数数组 argv。它是一个 char * 数组。

  • argv[0] 设置为 “ls”(程序名)。

  • argv[1] 设置为 “-l”(第一个参数)。

  • argv[2] 设置为 “/tmp”(第二个参数)。

  • 关键: 数组的最后一个元素必须是 NULL,以标记参数列表结束。

定义环境变量数组 envp。这里为了简化,直接使用了全局变量 environ,它指向当前进程的环境变量列表,从而使新程序继承所有环境变量。

调用 execve(pathname, argv, envp)。

关键: 如果 execve 成功,它会用 ls 程序替换当前进程,ls 程序开始执行,并且永远不会返回到 execve 之后的代码。

关键: 如果 execve 失败(例如,/bin/ls 文件不存在或不可执行),它会返回 -1,并设置 errno。

因此,execve 之后的代码只有在失败时才会执行。这里打印错误信息并调用 exit(EXIT_FAILURE) 退出程序。

示例 2:使用 execve 执行自定义程序并传递自定义环境变量

这个例子演示了如何执行一个自定义程序,并向其传递一组自定义的环境变量。

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
// execve_custom.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

// 假设你有一个简单的 C 程序 my_program.c 如下,并已编译为 my_program:
/*
// my_program.c
#include <stdio.h>
#include <stdlib.h> // getenv

int main(int argc, char *argv&#91;], char *envp&#91;]) {
printf("--- My Custom Program Started ---\n");
printf("Arguments received (argc=%d):\n", argc);
for (int i = 0; i < argc; ++i) {
printf(" argv&#91;%d]: %s\n", i, argv&#91;i]);
}

// 打印特定的环境变量
char *my_env = getenv("MY_CUSTOM_ENV");
char *lang_env = getenv("LANG");
printf("\nEnvironment variables:\n");
printf(" MY_CUSTOM_ENV: %s\n", my_env ? my_env : "(not set)");
printf(" LANG: %s\n", lang_env ? lang_env : "(not set)");

printf("--- My Custom Program Finished ---\n");
return 42;
}
*/

int main() {
char *pathname = "./my_program"; // 假设 my_program 在当前目录

// 1. 定义命令行参数
char *argv&#91;] = { "my_program_alias", "arg1", "arg2 with spaces", NULL };

// 2. 定义自定义环境变量
// 注意:数组必须以 NULL 结尾
char *envp&#91;] = {
"MY_CUSTOM_ENV=Hello_From_Execve",
"LANG=C",
"PATH=/usr/local/bin:/usr/bin:/bin", // 覆盖 PATH
NULL
};

printf("Parent process preparing to execve '%s' with custom environment.\n", pathname);

// --- 关键: 调用 execve 并传递自定义环境 ---
execve(pathname, argv, envp);

// --- 如果执行到这里,说明 execve 失败 ---
perror("execve failed");
printf("Failed to execute '%s'. Make sure it exists and is executable.\n", pathname);
exit(EXIT_FAILURE);
}

如何测试:

首先,创建并编译 my_program.c:# 创建 my_program.c (内容如上注释所示) gcc -o my_program my_program.c chmod +x my_program # 确保可执行

编译并运行 execve_custom.c:gcc -o execve_custom execve_custom.c ./execve_custom

代码解释:

定义要执行的程序路径 pathname (“./my_program”)。

定义命令行参数 argv,包括一个别名和两个参数。

关键: 定义一个自定义的环境变量数组 envp。

  • 它包含三个环境变量:MY_CUSTOM_ENV, LANG, PATH。

  • 重要: 数组以 NULL 结尾。

调用 execve(pathname, argv, envp)。

如果成功,my_program 将被执行,并接收 argv 和 envp 中定义的参数和环境变量。

my_program 会打印接收到的参数和特定的环境变量值,证明 execve 正确传递了它们。

my_program 执行完毕后(返回 42),整个进程(包括 execve_custom)就结束了。

示例 3:fork + execve 经典范式

这个例子演示了 Unix/Linux 系统编程中最经典、最常用的模式:fork 创建子进程,然后在子进程中调用 execve 执行新程序。

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
// fork_execve.c
#include <sys/socket.h> // fork, wait
#include <sys/wait.h> // wait
#include <unistd.h> // execve, fork
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit

extern char **environ;

int main() {
pid_t pid;
char *pathname = "/bin/date"; // 执行 date 命令
char *argv&#91;] = { "date", "+%Y-%m-%d %H:%M:%S", NULL };
char **envp = environ;

printf("Parent process (PID: %d) is about to fork.\n", getpid());

// 1. 创建子进程
pid = fork();

if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE);

} else if (pid == 0) {
// --- 子进程 ---
printf("Child process (PID: %d) created.\n", getpid());

// 2. 在子进程中调用 execve 执行新程序
printf("Child (PID: %d) is about to execve '%s'.\n", getpid(), pathname);
execve(pathname, argv, envp);

// --- 如果代码执行到这里,说明 execve 失败 ---
perror("execve failed in child");
printf("Child process (PID: %d) exiting due to execve failure.\n", getpid());
// 子进程失败时应使用 _exit,而不是 exit
_exit(EXIT_FAILURE);

} else {
// --- 父进程 ---
printf("Parent process (PID: %d) created child (PID: %d).\n", getpid(), pid);

// 3. 父进程等待子进程结束
int status;
printf("Parent (PID: %d) is waiting for child (PID: %d) to finish...\n", getpid(), pid);

if (waitpid(pid, &status, 0) == -1) {
perror("waitpid failed");
exit(EXIT_FAILURE);
}

// 4. 检查子进程的退出状态
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("Parent: Child (PID: %d) exited normally with status %d.\n", pid, exit_code);
} else if (WIFSIGNALED(status)) {
int sig = WTERMSIG(status);
printf("Parent: Child (PID: %d) was terminated by signal %d.\n", pid, sig);
} else {
printf("Parent: Child (PID: %d) did not exit normally.\n", pid);
}

printf("Parent process (PID: %d) finished.\n", getpid());
}

return 0;
}

代码解释:

定义要执行的程序路径 (/bin/date) 和参数 (date +%Y-%m-%d %H:%M:%S)。

调用 fork() 创建子进程。

在子进程中 (pid == 0):

  • 调用 execve(pathname, argv, envp) 执行 date 命令。

  • 如果 execve 成功,子进程从此处消失,date 命令开始执行。

  • 如果 execve 失败,打印错误信息并调用 _exit(EXIT_FAILURE) 退出子进程。强调: 在 fork 的子进程中,失败时应使用 _exit 而非 exit。

在父进程中 (pid > 0):

  • 打印信息。

  • 调用 waitpid(pid, &status, 0) 等待特定的子进程 (pid) 结束。

waitpid 返回后,检查子进程的退出状态 status。

  • WIFEXITED(status): 检查子进程是否正常退出(通过 exit 或 return)。

  • WEXITSTATUS(status): 获取子进程的退出码。

  • WIFSIGNALED(status): 检查子进程是否被信号终止。

  • WTERMSIG(status): 获取终止子进程的信号编号。

根据退出状态打印相应信息。

父进程结束。

重要提示与注意事项:

永不返回: execve 成功时永远不会返回。这是其最根本的特性。

失败处理: execve 失败时返回 -1。必须检查返回值并处理错误,因为程序会继续执行 execve 之后的代码。

_exit vs exit: 在 fork 之后的子进程中,如果 execve 失败并需要退出,应调用 _exit() 而不是 exit()。因为 exit() 会执行一些清理工作(如调用 atexit 注册的函数、刷新 stdio 缓冲区),这在子进程中可能导致意外行为(例如,缓冲区被刷新两次)。

参数和环境数组: argv 和 envp 数组必须以 NULL 指针结尾。忘记 NULL 会导致未定义行为。

argv[0]: 按惯例,argv[0] 应该是程序的名字。虽然可以是任意字符串,但很多程序会使用它来确定自己的行为。

环境变量: envp 数组定义了新程序的完整环境。它不会自动继承父进程的环境,除非你显式地传递 environ。

PATH 搜索: execve 不会在 PATH 环境变量中搜索可执行文件。它要求 pathname 是一个完整的路径。如果需要 PATH 搜索功能,应使用 execvp 或 execvpe。

权限: 调用进程必须对 pathname 指定的文件具有执行权限。

文件描述符: execve 不会关闭当前进程中打开的文件描述符(除非它们设置了 FD_CLOEXEC 标志)。新程序会继承这些文件描述符。

exec 函数族选择:

  • 需要最精确控制(指定完整路径、自定义环境):使用 execve。

  • 需要 PATH 搜索:使用 execvp 或 execvpe。

  • 参数较少且希望列表形式:使用 execl 或 execlp。

  • 一般推荐:execv 或 execvp,因为它们使用数组形式,更灵活且不易出错。

总结:

execve 是 Linux 系统中执行新程序的核心机制。它通过完全替换当前进程的内存镜像来启动一个新的程序。理解其参数(路径、参数数组、环境数组)和永不返回的特性对于掌握进程执行和 Unix/Linux 编程范式至关重要。它通常与 fork 结合使用,形成创建并运行新进程的经典模式。虽然有更高级的 exec 函数变体,但 execve 是它们的基础。

exit_group系统调用及示例

exit_group - 终止线程组

函数介绍

exit_group是一个Linux系统调用,用于终止整个线程组(进程组),而不仅仅是当前线程。它是多线程程序中用于整体退出的重要机制。

函数原型

1
2
3
4
5
#include <sys/syscall.h>
#include <unistd.h>

void exit_group(int status);

功能

终止调用线程所属的整个线程组,所有线程都会退出。

参数

  • int status: 退出状态码,传递给父进程

返回值

  • 无返回值(函数不返回)

特殊限制

  • 是Linux特有的系统调用

  • 需要通过syscall调用

  • 终止整个线程组,不只是当前线程

相似函数

  • exit(): 终止当前进程(单线程环境)

  • _exit(): 立即终止当前进程

  • pthread_exit(): 终止当前线程

示例代码

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <pthread.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

// 系统调用包装
static void exit_group_wrapper(int status) {
syscall(__NR_exit_group, status);
}

// 线程函数
void* thread_function(void* arg) {
int thread_id = *(int*)arg;

printf("线程 %d 启动\n", thread_id);

// 模拟工作
sleep(1);

if (thread_id == 2) {
printf("线程 %d 调用exit_group,整个线程组将退出\n", thread_id);
exit_group_wrapper(42); // 整个进程组退出
// 下面的代码不会执行
printf("这行代码不会被执行\n");
}

// 其他线程继续工作
sleep(2);
printf("线程 %d 完成工作\n", thread_id);

return NULL;
}

int main() {
printf("=== Exit_group 函数示例 ===\n");
printf("当前进程PID: %d\n", getpid());

// 示例1: 多线程环境中的exit_group
printf("\n示例1: 多线程环境中的exit_group\n");

pthread_t threads&#91;3];
int thread_ids&#91;3] = {1, 2, 3};

// 创建多个线程
for (int i = 0; i < 3; i++) {
if (pthread_create(&threads&#91;i], NULL, thread_function, &thread_ids&#91;i]) != 0) {
perror("创建线程失败");
exit(EXIT_FAILURE);
}
printf("创建线程 %d\n", thread_ids&#91;i]);
}

// 等待线程(实际上不会等到,因为线程2会调用exit_group)
printf("主线程等待子线程...\n");

// 这里程序会因为exit_group而终止,不会执行到下面
for (int i = 0; i < 3; i++) {
pthread_join(threads&#91;i], NULL);
}

printf("所有线程完成\n"); // 这行不会执行
return 0; // 这行也不会执行
}

// 单独的测试函数
void test_exit_group_behavior() {
printf("\n=== Exit_group 行为测试 ===\n");

pid_t pid = fork();
if (pid == -1) {
perror("fork失败");
return;
}

if (pid == 0) {
// 子进程
printf("子进程PID: %d\n", getpid());

// 创建多个线程
pthread_t threads&#91;2];

// 线程1
pthread_create(&threads&#91;0], NULL, &#91;](void* arg) -> void* {
printf("线程1开始工作\n");
sleep(3); // 比主线程工作时间长
printf("线程1完成工作\n"); // 这行可能不会执行
return NULL;
}, NULL);

// 线程2(主线程模拟)
printf("主线程工作1秒后调用exit_group\n");
sleep(1);

printf("调用exit_group(100)\n");
exit_group_wrapper(100);

// 这些代码不会执行
printf("这行不会被执行\n");
pthread_join(threads&#91;0], NULL);

} else {
// 父进程等待子进程
int status;
pid_t result = waitpid(pid, &status, 0);
if (result != -1) {
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("子进程通过exit_group退出,退出码: %d\n", exit_code);
} else if (WIFSIGNALED(status)) {
int signal = WTERMSIG(status);
printf("子进程被信号 %d 终止\n", signal);
}
}
}
}

// exit vs exit_group 对比
void compare_exit_functions() {
printf("\n=== Exit vs Exit_group 对比 ===\n");

printf("exit() 行为:\n");
printf(" - 单线程: 终止整个进程\n");
printf(" - 多线程: 终止调用线程,其他线程继续运行\n");
printf(" - 执行清理函数\n");
printf(" - 刷新缓冲区\n\n");

printf("exit_group() 行为:\n");
printf(" - 单线程: 终止整个进程\n");
printf(" - 多线程: 终止整个线程组(所有线程)\n");
printf(" - 不执行清理函数\n");
printf(" - 不刷新缓冲区\n");
printf(" - 立即终止\n\n");

printf("pthread_exit() 行为:\n");
printf(" - 终止调用线程\n");
printf(" - 其他线程继续运行\n");
printf(" - 如果是最后一个线程,终止进程\n\n");
}

// 实际应用场景演示
void demonstrate_real_world_usage() {
printf("\n=== 实际应用场景 ===\n");

printf("exit_group的典型使用场景:\n");
printf("1. 多线程程序的整体错误处理\n");
printf("2. 资源严重不足时的紧急退出\n");
printf("3. 接收到致命信号时\n");
printf("4. 线程检测到不可恢复的错误\n\n");

// 模拟错误处理场景
printf("错误处理示例:\n");
printf("void handle_critical_error(int error_code) {\n");
printf(" log_error(\"严重错误: %%d\\n\", error_code);\n");
printf(" // 通知所有线程立即退出\n");
printf(" exit_group(error_code);\n");
printf("}\n\n");

printf("信号处理示例:\n");
printf("void signal_handler(int sig) {\n");
printf(" if (sig == SIGSEGV || sig == SIGBUS) {\n");
printf(" // 段错误,立即退出整个进程\n");
printf(" exit_group(128 + sig);\n");
printf(" }\n");
printf("}\n\n");
}

// 测试exit_group与信号的关系
void test_exit_group_with_signals() {
printf("\n=== Exit_group 与信号 ===\n");

printf("exit_group与信号的关系:\n");
printf("1. exit_group不被信号中断\n");
printf("2. 调用后立即终止,不处理待处理信号\n");
printf("3. 退出状态通过wait机制传递\n");
printf("4. 不执行信号处理程序\n\n");

// 演示信号处理
printf("信号处理中的使用:\n");
printf("在信号处理程序中使用exit_group:\n");
printf("void sigsegv_handler(int sig) {\n");
printf(" write(STDERR_FILENO, \"段错误!\\n\", 8);\n");
printf(" exit_group(128 + SIGSEGV);\n");
printf("}\n\n");
}

int main_comprehensive_test() {
printf("=== Exit_group 完整测试 ===\n");

// 运行各个测试
test_exit_group_behavior();
compare_exit_functions();
demonstrate_real_world_usage();
test_exit_group_with_signals();

printf("\n=== 总结 ===\n");
printf("exit_group的特点:\n");
printf("1. Linux特有系统调用\n");
printf("2. 终止整个线程组\n");
printf("3. 立即终止,不执行清理\n");
printf("4. 不刷新缓冲区\n");
printf("5. 适用于紧急退出场景\n\n");

printf("使用场景:\n");
printf("1. 多线程程序整体退出\n");
printf("2. 严重错误的紧急处理\n");
printf("3. 信号处理中的快速退出\n");
printf("4. 资源不足时的优雅退出\n\n");

printf("注意事项:\n");
printf("1. 需要通过syscall调用\n");
printf("2. 不执行atexit注册的函数\n");
printf("3. 不刷新标准I/O缓冲区\n");
printf("4. 所有线程立即终止\n");
printf("5. 慎重使用,可能导致数据丢失\n\n");

printf("与相关函数的区别:\n");
printf("- exit(): 执行清理,适用于正常退出\n");
printf("- _exit(): 立即退出,但只影响当前线程\n");
printf("- exit_group(): 立即退出整个线程组\n");
printf("- pthread_exit(): 只退出当前线程\n\n");

return 0;
}

https://www.calcguide.tech/2025/08/09/exit-group系统调用及示例

exit系统调用及示例

好的,我们继续按照您的列表顺序,介绍下一个函数。

1. 函数介绍

exit 是一个 C 标准库函数(而非直接的系统调用,但它会调用底层的 _exit 系统调用),用于终止调用它的当前进程。

你可以把 exit 想象成主角在电影结尾谢幕并优雅退场:

  • 主角(当前进程)完成了它的表演(执行了所有代码)。

  • 它调用 exit,告诉导演(操作系统):“我的戏演完了,现在我要离开了。”

  • 在正式退场前,主角可能会鞠躬致谢(执行清理工作),然后离开舞台(进程终止)。

exit 不仅会终止进程,还会执行一些标准的清理(cleanup)操作,然后将控制权交还给操作系统。

2. 函数原型

1
2
3
4
#include <stdlib.h> // 必需 (C 标准库)

void exit(int status);

注意: exit 是 C 标准库函数。其底层通常会调用 Linux 系统调用 _exit。

3. 功能

  • 终止进程: 立即终止调用 exit 的进程。

执行清理: 在终止进程之前,exit 会执行一系列标准的清理操作:

  • 调用退出处理函数: 按照与注册时相反的顺序(后注册先调用),调用所有通过 atexit 或 on_exit 注册的函数。

  • 刷新并关闭标准 I/O 流: 自动刷新所有输出流(如 stdout, stderr)的缓冲区,确保所有待写数据都被写出。然后关闭所有标准 I/O 流。

返回状态码: 将 status 参数作为进程的退出状态(exit status)返回给父进程。

  • 按照惯例,0 表示成功,非 0 值通常表示某种错误或异常。

不返回: exit 函数永远不会返回到调用它的函数。一旦调用,进程即终止。

4. 参数

int status: 这是进程的退出状态码。

  • 这是一个整数值,它会被传递给父进程(通常是启动该进程的 shell 或父进程)。

惯例:

  • EXIT_SUCCESS (通常定义为 0): 表示程序成功执行完毕。

  • EXIT_FAILURE (通常定义为 1): 表示程序执行失败。

  • 自定义值: 你可以使用 0-255 范围内的任何整数来表示特定的错误类型(例如,2 表示配置错误,3 表示文件未找到等)。超出 0-255 范围的值会被模 256 处理。

5. 返回值

  • void: exit 函数没有返回值,因为它永远不会返回。

6. 相似函数,或关联函数

  • _exit: 这是一个直接的 Linux 系统调用。它立即终止进程,不执行任何标准 I/O 缓冲区刷新或 atexit 注册函数的调用。它只关闭文件描述符并返回 status 给父进程。在 fork 之后的子进程中,如果 exec 失败,通常推荐使用 _exit 而非 exit。

  • atexit: 用于注册在进程正常终止(通过 exit)时要调用的函数。

  • on_exit: 类似于 atexit,但注册的函数可以接收 status 和一个用户提供的参数。

  • return from main: 在 main 函数中执行 return status; 等价于调用 exit(status)。

  • abort: 立即异常终止进程,通常会产生核心转储(core dump)文件。

7. 示例代码

示例 1:基本的 exit 使用和 atexit 注册清理函数

这个例子演示了 exit 如何终止进程,并展示 atexit 注册的清理函数是如何被调用的。

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
// exit_atexit.c
#include <stdlib.h> // exit, atexit, EXIT_SUCCESS, EXIT_FAILURE
#include <stdio.h> // printf, perror

// 定义两个清理函数
void cleanup_function_1(void) {
printf("Cleanup function 1 is running.\n");
}

void cleanup_function_2(void) {
printf("Cleanup function 2 is running.\n");
}

int main() {
printf("Main function started.\n");

// 1. 注册清理函数
// 注意注册顺序: 2 -> 1
if (atexit(cleanup_function_1) != 0) {
perror("atexit for function 1 failed");
// 即使注册失败,程序也可以继续,但这不是好习惯
exit(EXIT_FAILURE);
}

if (atexit(cleanup_function_2) != 0) {
perror("atexit for function 2 failed");
exit(EXIT_FAILURE);
}

printf("Cleanup functions registered.\n");

// 2. 执行一些工作
printf("Performing some work in main...\n");
for (int i = 0; i < 3; ++i) {
printf(" Work step %d\n", i + 1);
}

// 3. 刷新 stdout 缓冲区 (可选,exit 会自动做)
fflush(stdout);

// 4. 正常退出,触发清理
printf("Main function finished. Calling exit(EXIT_SUCCESS).\n");
exit(EXIT_SUCCESS); // 等价于 return EXIT_SUCCESS; from main

// --- 下面的代码永远不会执行 ---
printf("This line will never be printed.\n");
}

代码解释:

定义了两个简单的清理函数 cleanup_function_1 和 cleanup_function_2,它们只是打印一条消息。

在 main 函数中,使用 atexit() 注册这两个清理函数。

  • 注意注册顺序:先注册 cleanup_function_1,再注册 cleanup_function_2。

执行一些模拟工作。

调用 exit(EXIT_SUCCESS)。

关键: 程序不会打印 “This line will never be printed.”。

关键: exit 会按注册的相反顺序调用清理函数。因此,会先打印 “Cleanup function 2 is running.”,然后是 “Cleanup function 1 is running.”。

exit 会自动刷新 stdout 的缓冲区。

进程终止,返回状态码 0 给父进程。

示例 2:exit 与 _exit 的区别 (在 fork 子进程中)

这个例子通过对比演示了在 fork 子进程中使用 exit 和 _exit 的区别。

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
// exit_vs__exit.c
#include <sys/socket.h> // fork
#include <unistd.h> // _exit, fork
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit

int main() {
pid_t pid;

// 打印一些内容到 stdout,但不换行,数据会留在缓冲区
printf("Parent process (PID: %d) printing without newline. Buffer content: ");

pid = fork();

if (pid == -1) {
perror("fork failed");
exit(EXIT_FAILURE); // 父进程失败,使用 exit

} else if (pid == 0) {
// --- 子进程 ---
printf("This is child process (PID: %d).\n", getpid());

// 子进程打印到 stdout,数据会留在缓冲区
printf("Child is about to terminate. ");

// --- 关键区别 ---
// 使用 exit: 会刷新 stdio 缓冲区
// printf("Child calling exit(EXIT_SUCCESS).\n");
// exit(EXIT_SUCCESS);

// 使用 _exit: 不会刷新 stdio 缓冲区
printf("Child calling _exit(EXIT_SUCCESS).\n");
_exit(EXIT_SUCCESS); // 子进程推荐使用 _exit

} else {
// --- 父进程 ---
printf("Parent continues after fork.\n");
printf("Parent process (PID: %d) finished.\n", getpid());
// 父进程正常退出,会刷新自己的缓冲区
exit(EXIT_SUCCESS);
}

// 这行代码不会被执行
return 0;
}

代码解释:

父进程首先打印一条消息到 stdout,但没有换行符 (\n)。在大多数系统上,stdout 在连接到终端时是行缓冲的,这意味着没有换行符的数据会暂时保存在stdio 缓冲区中,而不会立即显示在屏幕上。

调用 fork() 创建子进程。

在子进程中:

  • 打印一条消息 “This is child process …”。

  • 再打印一条消息 “Child is about to terminate. “(同样没有换行符)。

关键: 调用 _exit(EXIT_SUCCESS)。

  • _exit 会立即终止子进程。

  • 它不会刷新 stdio 缓冲区。

  • 因此,子进程中打印但未刷新的 “Child is about to terminate. “ 不会出现在输出中。

在父进程中:

  • 打印消息。

  • 调用 exit(EXIT_SUCCESS)。

  • exit 会刷新父进程的 stdio 缓冲区。

  • 因此,父进程中打印但未刷新的 “Parent process (PID: …) printing without newline. Buffer content: “ 会因为 exit 的刷新操作而被打印出来。

  • 然后打印 “Parent continues …” 和 “Parent process … finished.”。

运行结果:

1
2
3
4
5
Parent process (PID: 12345) printing without newline. Buffer content: This is child process (PID: 12346).
Child is about to terminate. Child calling _exit(EXIT_SUCCESS).
Parent continues after fork.
Parent process (PID: 12345) finished.

分析:

  • 父进程的缓冲区内容 “Buffer content: “ 被打印了,因为父进程的 exit 调用刷新了它。

  • 子进程的缓冲区内容 “Child is about to terminate. “ 没有被打印,因为子进程调用的 _exit 没有刷新缓冲区。

如果子进程调用 exit(EXIT_SUCCESS):

  • 子进程的 exit 也会尝试刷新缓冲区。

  • 这会导致 “Child is about to terminate. “ 被打印。

  • 但是,如果父进程也在运行并且也调用 exit,两个进程都试图刷新 stdout,可能会导致输出混乱或重复(因为它们共享了 fork 时的缓冲区状态)。虽然这个简单例子可能看不出问题,但在更复杂的情况下,这可能导致不可预测的行为。

  • 因此,在 fork 的子进程中,如果后续调用 exec 失败需要退出,强烈推荐使用 _exit 以避免这种潜在的 stdio 状态混乱。

重要提示与注意事项:

永不返回: exit 调用后,当前进程立即终止,函数不返回。

清理工作: exit 会执行重要的清理工作(atexit 函数、刷新 stdio)。这是它与 _exit 的主要区别。

_exit 在子进程中: 在 fork 之后的子进程中,如果需要在 exec 失败后退出,应使用 _exit 而非 exit,以避免刷新共享的 stdio 缓冲区。

main 中的 return: 在 main 函数中,return status; 等价于 exit(status);。

状态码: 使用 EXIT_SUCCESS 和 EXIT_FAILURE 宏比直接使用数字更具可读性和可移植性。

atexit 注册顺序: atexit 注册的函数在 exit 时按后进先出(LIFO)的顺序被调用。

stdio 缓冲区: 理解 exit 会刷新缓冲区,而 _exit 不会,对于避免输出混乱至关重要。

总结:

exit 是 C 程序终止的标准方式。它不仅终止进程,还负责任地执行清理工作,确保资源得到释放,输出得到刷新。理解其与系统调用 _exit 的区别,尤其是在多进程编程中,对于编写健壮的程序非常重要。

fadvise64系统调用及示例

fadvise64 - 文件访问建议

函数介绍

fadvise64是一个Linux系统调用,用于向内核提供关于文件访问模式的建议。它帮助内核优化文件I/O操作,提高性能。

函数原型

1
2
3
4
5
6
#include <fcntl.h>
#include <sys/syscall.h>
#include <unistd.h>

int fadvise64(int fd, off_t offset, off_t len, int advice);

功能

向内核提供文件访问模式建议,帮助内核优化缓存和预读策略。

参数

  • int fd: 文件描述符

  • off_t offset: 建议适用的文件起始偏移量

  • off_t len: 建议适用的文件长度(0表示到文件末尾)

int advice: 访问建议类型

  • POSIX_FADV_NORMAL: 普通访问模式(默认)

  • POSIX_FADV_SEQUENTIAL: 顺序访问

  • POSIX_FADV_RANDOM: 随机访问

  • POSIX_FADV_NOREUSE: 数据只访问一次

  • POSIX_FADV_WILLNEED: 数据即将被访问

  • POSIX_FADV_DONTNEED: 数据不再需要

返回值

  • 成功时返回0

  • 失败时返回-1,并设置errno

特殊限制

  • 需要Linux 2.5.60以上内核支持

  • 某些文件系统可能不完全支持

  • 建议只是提示,内核可能忽略

相似函数

  • madvise(): 内存访问建议

  • readahead(): 文件预读

  • posix_fadvise(): POSIX标准版本

示例代码

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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <string.h>
#include <sys/syscall.h>

// 系统调用包装
static int fadvise64_wrapper(int fd, off_t offset, off_t len, int advice) {
return syscall(__NR_fadvise64, fd, offset, len, advice);
}

// 创建测试文件
int create_test_file(const char* filename, size_t size) {
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("创建测试文件失败");
return -1;
}

// 写入测试数据
char* buffer = malloc(4096);
if (buffer) {
memset(buffer, 'A', 4096);
for (size_t i = 0; i < size; i += 4096) {
size_t write_size = (size - i > 4096) ? 4096 : (size - i);
write(fd, buffer, write_size);
}
free(buffer);
}

return fd;
}

int main() {
int fd;
int result;

printf("=== Fadvise64 函数示例 ===\n");

// 示例1: 基本使用
printf("\n示例1: 基本使用\n");

// 创建大文件用于测试
fd = create_test_file("test_fadvise64.dat", 1024 * 1024); // 1MB
if (fd == -1) {
exit(EXIT_FAILURE);
}
printf("创建测试文件: test_fadvise64.dat (1MB)\n");

close(fd);

// 重新打开文件进行测试
fd = open("test_fadvise64.dat", O_RDONLY);
if (fd == -1) {
perror("打开测试文件失败");
unlink("test_fadvise64.dat");
exit(EXIT_FAILURE);
}
printf("打开测试文件进行fadvise64测试\n");

// 示例2: 不同的访问建议
printf("\n示例2: 不同的访问建议\n");

// POSIX_FADV_NORMAL - 普通访问模式
result = fadvise64_wrapper(fd, 0, 0, POSIX_FADV_NORMAL);
if (result == 0) {
printf("设置POSIX_FADV_NORMAL成功\n");
} else {
printf("设置POSIX_FADV_NORMAL失败: %s\n", strerror(errno));
}

// POSIX_FADV_SEQUENTIAL - 顺序访问
result = fadvise64_wrapper(fd, 0, 1024*1024, POSIX_FADV_SEQUENTIAL);
if (result == 0) {
printf("设置POSIX_FADV_SEQUENTIAL成功\n");
printf("提示内核将进行顺序访问,优化预读策略\n");
}

// POSIX_FADV_RANDOM - 随机访问
result = fadvise64_wrapper(fd, 0, 1024*1024, POSIX_FADV_RANDOM);
if (result == 0) {
printf("设置POSIX_FADV_RANDOM成功\n");
printf("提示内核将进行随机访问,减少预读\n");
}

// POSIX_FADV_WILLNEED - 数据即将被访问
result = fadvise64_wrapper(fd, 0, 64*1024, POSIX_FADV_WILLNEED);
if (result == 0) {
printf("设置POSIX_FADV_WILLNEED成功\n");
printf("提示内核预读前64KB数据\n");
}

// POSIX_FADV_DONTNEED - 数据不再需要
result = fadvise64_wrapper(fd, 0, 64*1024, POSIX_FADV_DONTNEED);
if (result == 0) {
printf("设置POSIX_FADV_DONTNEED成功\n");
printf("提示内核可以丢弃前64KB数据的缓存\n");
}

// POSIX_FADV_NOREUSE - 数据只访问一次
result = fadvise64_wrapper(fd, 64*1024, 64*1024, POSIX_FADV_NOREUSE);
if (result == 0) {
printf("设置POSIX_FADV_NOREUSE成功\n");
printf("提示内核64KB-128KB范围的数据只访问一次\n");
}

// 示例3: 错误处理演示
printf("\n示例3: 错误处理演示\n");

// 使用无效的文件描述符
result = fadvise64_wrapper(999, 0, 1024, POSIX_FADV_NORMAL);
if (result == -1) {
if (errno == EBADF) {
printf("无效文件描述符错误处理正确: %s\n", strerror(errno));
}
}

// 使用无效的建议类型
result = fadvise64_wrapper(fd, 0, 1024, 999);
if (result == -1) {
if (errno == EINVAL) {
printf("无效建议类型错误处理正确: %s\n", strerror(errno));
}
}

// 使用负的偏移量
result = fadvise64_wrapper(fd, -1024, 1024, POSIX_FADV_NORMAL);
if (result == -1) {
printf("负偏移量处理: %s\n", strerror(errno));
}

// 示例4: 实际使用场景演示
printf("\n示例4: 实际使用场景演示\n");

// 场景1: 大文件顺序读取
printf("场景1: 大文件顺序读取优化\n");
printf("处理大日志文件的代码示例:\n");
printf("int process_large_log(const char* filename) {\n");
printf(" int fd = open(filename, O_RDONLY);\n");
printf(" if (fd == -1) return -1;\n");
printf(" \n");
printf(" // 提示内核将顺序访问整个文件\n");
printf(" posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);\n");
printf(" \n");
printf(" // 读取处理文件...\n");
printf(" char buffer&#91;8192];\n");
printf(" ssize_t bytes;\n");
printf(" while ((bytes = read(fd, buffer, sizeof(buffer))) > 0) {\n");
printf(" // 处理数据\n");
printf(" }\n");
printf(" \n");
printf(" close(fd);\n");
printf(" return 0;\n");
printf("}\n\n");

// 场景2: 随机访问数据库文件
printf("场景2: 随机访问数据库文件\n");
printf("数据库文件访问优化:\n");
printf("int access_database_file(const char* filename) {\n");
printf(" int fd = open(filename, O_RDWR);\n");
printf(" if (fd == -1) return -1;\n");
printf(" \n");
printf(" // 提示内核将随机访问文件\n");
printf(" posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM);\n");
printf(" \n");
printf(" // 根据需要预读特定区域\n");
printf(" posix_fadvise(fd, index_offset, index_size, POSIX_FADV_WILLNEED);\n");
printf(" \n");
printf(" // 访问完成后释放不需要的缓存\n");
printf(" posix_fadvise(fd, old_data_offset, old_data_size, POSIX_FADV_DONTNEED);\n");
printf(" \n");
printf(" close(fd);\n");
printf(" return 0;\n");
printf("}\n\n");

// 示例5: 不同建议类型的效果说明
printf("示例5: 不同建议类型的效果说明\n");

printf("POSIX_FADV_NORMAL:\n");
printf(" - 默认访问模式\n");
printf(" - 使用系统默认的预读和缓存策略\n");
printf(" - 适用于一般情况\n\n");

printf("POSIX_FADV_SEQUENTIAL:\n");
printf(" - 优化顺序访问\n");
printf(" - 增加预读量\n");
printf(" - 适用于大文件顺序读取\n");
printf(" - 提高顺序读取性能\n\n");

printf("POSIX_FADV_RANDOM:\n");
printf(" - 优化随机访问\n");
printf(" - 减少或禁用预读\n");
printf(" - 适用于数据库、索引文件\n");
printf(" - 减少不必要的内存占用\n\n");

printf("POSIX_FADV_NOREUSE:\n");
printf(" - 数据只访问一次\n");
printf(" - 访问后尽快释放缓存\n");
printf(" - 适用于一次性处理的大文件\n");
printf(" - 节省内存资源\n\n");

printf("POSIX_FADV_WILLNEED:\n");
printf(" - 数据即将被访问\n");
printf(" - 提前预读数据到缓存\n");
printf(" - 适用于已知访问模式的场景\n");
printf(" - 减少实际访问时的等待\n\n");

printf("POSIX_FADV_DONTNEED:\n");
printf(" - 数据不再需要\n");
printf(" - 尽快释放缓存空间\n");
printf(" - 适用于处理完成的数据\n");
printf(" - 释放系统资源\n\n");

// 示例6: 性能测试演示
printf("示例6: 性能影响演示\n");

printf("fadvise64对性能的影响:\n");
printf("1. 正确使用可显著提高I/O性能\n");
printf("2. 错误使用可能导致性能下降\n");
printf("3. 效果因文件系统和硬件而异\n");
printf("4. 大文件效果更明显\n");
printf("5. 需要根据实际访问模式选择\n\n");

// 示例7: 实际应用建议
printf("示例7: 实际应用建议\n");

printf("使用fadvise64的最佳实践:\n");
printf("1. 在文件打开后尽早设置建议\n");
printf("2. 根据实际访问模式选择合适的建议\n");
printf("3. 对于大文件效果更明显\n");
printf("4. 不要过度使用,避免增加系统负担\n");
printf("5. 在长时间运行的应用中适时调整\n");
printf("6. 测试不同建议对性能的影响\n\n");

printf("常见应用场景:\n");
printf("- 大文件处理和分析\n");
printf("- 数据库系统\n");
printf("- 日志处理系统\n");
printf("- 备份和归档工具\n");
printf("- 媒体播放器\n");
printf("- 科学计算应用\n\n");

// 示例8: 与相关函数的对比
printf("示例8: 与相关函数的对比\n");

printf("fadvise64 vs madvise:\n");
printf("fadvise64:\n");
printf(" - 针对文件I/O\n");
printf(" - 影响文件缓存策略\n");
printf(" - 在文件描述符上操作\n\n");

printf("madvise:\n");
printf(" - 针对内存映射\n");
printf(" - 影响内存管理策略\n");
printf(" - 在内存地址上操作\n\n");

printf("fadvise64 vs readahead:\n");
printf("fadvise64:\n");
printf(" - 更通用的建议机制\n");
printf(" - 支持多种访问模式\n");
printf(" - 可以指定文件区域\n\n");

printf("readahead:\n");
printf(" - 专门用于预读\n");
printf(" - 立即执行预读操作\n");
printf(" - 较为直接但不够灵活\n\n");

// 清理资源
close(fd);
unlink("test_fadvise64.dat");

printf("总结:\n");
printf("fadvise64是Linux提供的文件访问优化机制\n");
printf("通过向内核提供访问建议来优化性能\n");
printf("支持多种访问模式的优化\n");
printf("是处理大文件和特定访问模式的重要工具\n");
printf("需要根据实际应用场景合理使用\n");

return 0;
}

IP协议栈在进行IP分片重组时注意事项

IP协议栈在处理IP分片(IP Fragmentation)时,对分片长度的设定遵循一系列规范,这些规范主要由 IPv4 和 IPv6 协议标准定义,特别是 RFC 791(IPv4)和 RFC 2460(IPv6)。以下是关于IP分片长度设定的主要规范:

一、IPv4 分片长度规范(RFC 791)

1. 最大传输单元(MTU)限制

  • 每个链路层网络有其最大传输单元(MTU),如以太网的MTU通常为 1500字节。

  • 当IP数据报的大小超过出接口的MTU时,IP层必须进行分片。

2. IP头部中的关键字段

IPv4头部包含以下与分片相关的字段:

字段长度作用Identification16位标识同一原始数据报的所有分片Flags3位包含:DF(Don’t Fragment)、MF(More Fragments)Fragment Offset13位指示该分片在原始数据报中的偏移量(以8字节为单位)

3. 分片长度的基本要求

  • 分片的数据部分必须是8字节的整数倍:

  • 因为 Fragment Offset 字段以 8字节 为单位,所以每个分片(除最后一个)的数据部分必须能被8整除。

  • 例如:若MTU=1500,IP头部20字节,则数据部分最多1480字节。但为了满足8字节对齐,实际使用的数据部分为 1480 - (1480 % 8) = 1472 字节。

  • 所以第一个分片可携带1472字节数据,偏移为0;下一个偏移为1472/8=184,依此类推。

  • 最小分片大小:

  • 每个分片必须至少携带 8字节数据(否则无法形成有效分片),加上20字节IP头部,最小分片总长度为28字节。

  • 实际中,大多数链路要求最小帧长更高(如以太网为64字节),因此实际分片不会太小。

4. DF位(Don’t Fragment)

  • 如果IP头部中 DF=1,路由器不能对该数据报进行分片。

  • 若数据报过大且DF=1,则路由器丢弃该报文,并返回 ICMP “Fragmentation Needed” 错误(类型3,代码4)。

5. MF位(More Fragments)

  • 除最后一个分片外,所有分片的 MF=1。

  • 最后一个分片的 MF=0。

二、IPv6 分片规范(RFC 2460)

1. 禁止在中间路由器上分片

  • IPv6 禁止中间路由器对数据报进行分片。

  • 分片只能由源主机进行(使用分片扩展头部)。

2. 路径MTU发现(PMTUD)是必须的

  • IPv6依赖路径MTU发现机制(PMTUD)来确定整条路径上的最小MTU。

  • 源主机根据PMTUD结果决定是否需要分片。

3. 分片扩展头部

  • 使用“分片扩展头部”(Fragment Extension Header)来支持分片。

  • 包含:

  • Identificaiton(32位)

  • Offset(13位,同IPv4,以8字节为单位)

  • M flag(1位,表示是否有更多分片)

4. 分片对齐要求

  • 与IPv4相同,分片偏移以8字节为单位,因此每个分片的数据部分(除最后一个)必须是8字节的整数倍。

5. 最小链路MTU

  • IPv6规定所有链路必须支持至少 1280字节 的MTU。

  • 实际中推荐使用 1500字节 以太网MTU。

三、通用规范总结

规范IPv4IPv6是否允许中间路由器分片是否(仅源主机可分片)分片最小数据单元8字节对齐8字节对齐分片偏移单位8字节8字节分片标识字段16位32位是否强制使用PMTUD否(可选)是(推荐或必需)DF位作用控制是否允许分片无DF位,但PMTUD隐含此功能

四、实际应用中的建议

避免分片:分片会增加丢包重传开销、重组失败风险,建议使用路径MTU发现(PMTUD)来避免分片。

TCP MSS(Maximum Segment Size):通常设置为 MTU - IP头 - TCP头 = 1500 - 20 - 20 = 1460 字节,以避免IP层分片。

防火墙和NAT设备可能丢弃分片包,导致通信失败。

五、参考标准

  • RFC 791 - Internet Protocol (IPv4)

  • RFC 2460 - Internet Protocol, Version 6 (IPv6)

  • RFC 1191 - Path MTU Discovery (IPv4)

  • RFC 1981 - Path MTU Discovery for IPv6

  • RFC 8200 - Updated IPv6 Specification

总结

IP协议栈对分片长度的核心规范包括:

  • 分片数据长度必须是8字节的整数倍(由Fragment Offset单位决定);

  • 每个分片包含IP头部+数据部分;

  • IPv4允许路由器分片,IPv6仅允许源主机分片;

  • 使用MTU和PMTUD控制分片行为,尽量避免分片以提高性能和可靠性。

合理设计应用数据大小、启用PMTUD、设置合适的MSS,是避免IP分片问题的关键。

https://www.calcguide.tech/2025/08/09/ip协议栈在进行ip分片重组时注意事项/

Uchardet库中的utf-8的置信度计算方法

Uchardet库中的utf-8的置信度计算方法好的,感谢您提供 nsUTF8Prober.cpp 的代码。这让我们能够清晰地看到 uchardet 中 UTF-8 编码探测器的具体实现,特别是其置信度计算逻辑。

代码分析:nsUTF8Prober 置信度计算

1. 核心逻辑

该探测器的核心思想是:通过验证 UTF-8 编码规则来判断文本是否为 UTF-8。它使用一个状态机 (mCodingSM) 来跟踪字节序列是否符合 UTF-8 规范。

  • Reset(): 初始化探测器状态,重置状态机、多字节字符计数器 (mNumOfMBChar) 和探测状态 (mState)。

HandleData(): 这是处理输入字节流的主要函数。

  • 它逐字节地将数据传递给内部的状态机 (mCodingSM->NextState(aBuf[i]))。

  • 状态机返回 eItsMe 表示检测到明确违反 UTF-8 规则的序列,探测器状态变为 eFoundIt(虽然名字是 FoundIt,但在这里实际上是“确认不是 UTF-8”)。

状态机返回 eStart 表示一个完整的 UTF-8 字符(可能是单字节 ASCII 或多字节序列)已被成功识别。

  • 如果这个字符是多字节的 (mCodingSM->GetCurrentCharLen() >= 2),则 mNumOfMBChar 计数器增加。

  • 代码还包含了构建 Unicode 码点 (currentCodePoint) 的逻辑,并将其存入 codePointBuffer(这可能用于后续更复杂的分析,或者是为了提供解码后的内容)。

其他状态 (eError, eItsMe 外的其他中间状态) 表示正在处理一个多字节序列。

关键优化: 在 HandleData 的末尾,有一个检查:if (mState == eDetecting) if (mNumOfMBChar > ENOUGH_CHAR_THRESHOLD && GetConfidence(0) > SHORTCUT_THRESHOLD) mState = eFoundIt; 这意味着,如果已经识别出足够多的多字节 UTF-8 字符 (mNumOfMBChar > 256),并且根据当前信息计算出的置信度也足够高 (GetConfidence(0) > SHORTCUT_THRESHOLD,虽然 SHORTCUT_THRESHOLD 在此文件中未定义,但通常在 .h 文件或主控逻辑中定义),探测器可以提前结束并确认文本很可能是 UTF-8。

2. 置信度计算 (GetConfidence)

这是最核心的部分,代码非常简洁:

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
#define ONE_CHAR_PROB   (float)0.50

float nsUTF8Prober::GetConfidence(int candidate)
{
// 如果识别出的多字节 UTF-8 字符少于 6 个
if (mNumOfMBChar < 6)
{
// 使用一种“悲观”模型计算置信度
float unlike = 0.5f; // 初始假设文本不像 UTF-8 的概率是 50%

// 对于每个已识别的多字节字符,都假设它“碰巧”符合 UTF-8 规则的概率是 50%
// 因此,所有 N 个字符都“碰巧”符合的概率是 (0.5)^N
for (PRUint32 i = 0; i < mNumOfMBChar; i++)
unlike *= ONE_CHAR_PROB; // ONE_CHAR_PROB = 0.5

// 置信度 = 1 - (文本不像 UTF-8 的概率)
// 即,文本像 UTF-8 的概率
return (float)1.0 - unlike;
}
else
{
// 如果已经识别出 6 个或更多多字节 UTF-8 字符
// 认为非常可能是 UTF-8,返回一个很高的置信度 (0.99)
return (float)0.99;
}
}

3. 置信度计算方法解析

nsUTF8Prober 的置信度计算采用了一种基于统计显著性的启发式方法:

少于 6 个多字节字符时 (Low Confidence Mode):

  • 它假设,如果一段文本不是 UTF-8,但恰好包含了 N 个看起来像有效 UTF-8 多字节序列的字节串,那么这种“巧合”发生的概率是 (0.5)^N。这里的 0.5 (ONE_CHAR_PROB) 是一个经验性的估计值,代表一个随机字节序列“碰巧”符合 UTF-8 多字节起始规则的概率。

  • 因此,它认为这段文本是 UTF-8 的置信度为 1 - (0.5)^N。

例子:

  • mNumOfMBChar = 0: confidence = 1.0 - 0.5 = 0.5 (完全不确定)

  • mNumOfMBChar = 1: confidence = 1.0 - (0.5 * 0.5) = 1.0 - 0.25 = 0.75

  • mNumOfMBChar = 2: confidence = 1.0 - (0.25 * 0.5) = 1.0 - 0.125 = 0.875

  • mNumOfMBChar = 3: confidence = 1.0 - (0.125 * 0.5) = 1.0 - 0.0625 = 0.9375

  • mNumOfMBChar = 5: confidence = 1.0 - (0.0625 * 0.5 * 0.5) = 1.0 - 0.015625 = 0.984375

这种方法直观地反映了:发现的有效 UTF-8 多字节字符越多,它就越不可能是巧合,因此置信度越高。

6 个或更多多字节字符时 (High Confidence Mode):

  • 一旦识别出 6 个或更多的多字节 UTF-8 字符,算法就认为有足够的证据表明文本是 UTF-8 编码的,因此直接返回一个非常高的、接近确定的置信度 0.99。这是一种优化和经验判断,认为达到这个数量级后,误判的可能性极低。

4. 总结

nsUTF8Prober 的置信度计算方法相对简单但有效:

  • 核心依据:识别出的有效 UTF-8 多字节字符的数量 (mNumOfMBChar)。

计算逻辑:

  • 少量 (<6):使用 (1 - (0.5)^N) 的公式,基于“巧合概率”的倒数来估算置信度。

  • 大量 (>=6):直接返回高置信度 0.99。

优点:

  • 计算简单快速。

  • 对于明显符合或不符合 UTF-8 规则的文本,能快速给出合理判断。

  • 通过状态机严格检查 UTF-8 规则,一旦发现违规立即否定。

特点:

  • 它更侧重于排除法:通过验证规则和统计显著性来增加“是 UTF-8”的信心,而不是直接计算“是 UTF-8”的概率。

  • 0.5 这个概率值是经验设定,可能在不同数据集下表现略有差异。

这种设计体现了 uchardet(及 chardet)注重实用性和效率的特点,通过简单的启发式规则在大多数情况下能给出较为可靠的置信度评估。

creat系统调用及示例

creat - 创建文件

函数介绍

creat系统调用用于创建新文件或截断已存在的文件。它是一个简化的文件创建函数,等价于open(path, O_WRONLY|O_CREAT|O_TRUNC, mode)。

函数原型

1
2
3
4
5
6
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

int creat(const char *pathname, mode_t mode);

功能

创建新文件或清空已存在的文件,返回只写权限的文件描述符。

参数

  • const char *pathname: 要创建的文件路径名

  • mode_t mode: 文件权限模式(如0644)

返回值

  • 成功时返回文件描述符(非负整数)

  • 失败时返回-1,并设置errno

特殊限制

  • 文件总是以只写模式打开

  • 如果文件已存在,会被截断为0字节

  • 不支持追加模式等高级选项

相似函数

  • open(): 更灵活的文件打开函数

  • mkstemp(): 创建安全的临时文件

示例代码

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
https://www.calcguide.tech/2025/08/08/creat系统调用及示例/#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>

int main() {
int fd;

printf("=== Creat函数示例 ===\n");

// 示例1: 基本文件创建
printf("示例1: 基本文件创建\n");
fd = creat("test_creat.txt", 0644);
if (fd == -1) {
perror("创建文件失败");
} else {
printf("成功创建文件,fd=%d\n", fd);
write(fd, "Hello from creat!", 17);
close(fd);
}

// 示例2: 重复创建(会截断原文件)
printf("示例2: 重复创建(截断原文件)\n");
fd = creat("test_creat.txt", 0644);
if (fd != -1) {
printf("重新创建文件(原内容被清空)\n");
write(fd, "New content", 11);
close(fd);
}

// 示例3: 权限测试
printf("示例3: 不同权限创建\n");
int fd1 = creat("file_600.txt", 0600); // 仅所有者读写
int fd2 = creat("file_644.txt", 0644); // 所有者读写,其他用户读
int fd3 = creat("file_666.txt", 0666); // 所有用户读写

if (fd1 != -1) {
printf("创建0600权限文件\n");
close(fd1);
}
if (fd2 != -1) {
printf("创建0644权限文件\n");
close(fd2);
}
if (fd3 != -1) {
printf("创建0666权限文件\n");
close(fd3);
}

// 清理测试文件
unlink("test_creat.txt");
unlink("file_600.txt");
unlink("file_644.txt");
unlink("file_666.txt");

return 0;
}

https://www.calcguide.tech/2025/08/08/creat系统调用及示例/

https://www.calcguide.tech/2025/08/26/linux开源软件路线图/