常用头文件

头文件作用
<stdio.h>标准输入输出库函数的声明,包括 printfscanffopen 等。
<stdlib.h>提供通用的工具函数,如动态内存分配 (malloc)、进程控制 (exit) 等。
<string.h>字符串操作函数的声明,如 strlenstrcpystrcmp 等。
<math.h>提供数学计算函数的声明,如 sincossqrt 等。
<time.h>时间和日期操作函数的声明,如 timelocaltimestrftime 等。
<unistd.h>定义 POSIX 标准的 API,如文件操作(readwrite)、进程管理等。
<fcntl.h>文件控制相关函数和常量的声明,如 open、文件访问模式常量等。
<sys/types.h>定义数据类型,如 pid_tuid_tgid_t 等,用于系统调用。
<sys/stat.h>文件状态操作函数的声明,如 statchmodmkdir 等。
<errno.h>定义全局错误变量 errno 及错误码常量,如 EACCESENOENT 等。
<signal.h>提供信号处理相关函数,如 signalraisekill 等。
<pthread.h>POSIX 线程库的头文件,用于多线程编程,如 pthread_create 等。
<sys/socket.h>提供套接字编程相关函数和结构体,如 socketbindconnect 等。
<netinet/in.h>定义网络地址相关的结构体和常量,如 sockaddr_inINADDR_ANY 等。
<arpa/inet.h>提供 IP 地址转换函数,如 inet_ptoninet_ntoa 等。
<sys/wait.h>提供进程等待相关函数,如 waitwaitpid 等。
<sys/mman.h>提供内存映射相关函数,如 mmapmunmap 等。
<sys/epoll.h>提供 epoll 多路复用接口函数,如 epoll_createepoll_wait 等。
<poll.h>提供 poll 多路复用接口的相关函数和常量。
<termios.h>提供终端 I/O 控制接口,如设置串口属性的 tcsetattr 等。
<sys/ioctl.h>提供设备 I/O 控制接口,如 ioctl 调用。
<ctype.h>定义字符操作函数,如 isalphaisdigittoupper 等。
<assert.h>提供断言功能,用于调试时检查程序中的逻辑条件。
<limits.h>定义各种数据类型的限制值,如 INT_MAXCHAR_BIT 等。
<float.h>定义浮点类型的限制值,如 FLT_MAXDBL_MIN 等。
<locale.h>提供本地化支持函数,如 setlocalelocaleconv 等。
<grp.h>提供组信息相关函数,如 getgrgidgetgrnam 等。
<pwd.h>提供用户信息相关函数,如 getpwnamgetpwuid 等。
<sys/resource.h>提供系统资源限制相关函数,如 getrlimitsetrlimit 等。

系统数据类型选录

数据类型SUSv3 类型需求描述
blkcnt_t有符号整型文件块数量(15.1 节)
blksize_t有符号整型文件块大小(15.1 节)
cc_t无符号整型终端特殊字符(62.4 节)
clock_t整型或浮点型实数以时钟周期计量的系统时间(10.7 节)
clockid_t运算类型之一针对 POSIX.1b 时钟和定时器函数的时钟标识符
comp_tSUSv3 未作规范经由压缩处理的时钟周期(28.1 节)
dev_t运算类型之一设备号,包含主、次设备号(15.1 节)
DIR无类型要求目录流(18.8 节)
fd_set结构类型select()(63.2.1 节)中的文件描述符集合
fsblkcnt_t无符号整型文件系统块数量(14.11 节)
fsfilcnt_t无符号整型文件数量(14.11 节)
gid_t整型数值型组标识符(8.3 节)
id_t整型用以存放标识符的通用类型,其大小至少可放置 pid_tuid_tgid_t 类型
in_addr_t32 位无符号整型IPv4 地址(59.4 节)
in_port_t16 位无符号整型IP 端口号(59.4 节)
ino_t无符号整型文件 i-node 号(15.1 节)
key_t运算类型之一System V IPC 键(45.2 节)
mode_t整型文件权限及类型(15.1 节)
mqd_t无类型要求POSIX 消息队列描述符
msglen_t无符号整型System V 消息队列所允许的字节数(46.4 节)
msgqnum_t无符号整型System V 消息队列中的消息数量(46.4 节)
nfds_t无符号整型poll()(63.2.2 节)中的文件描述符数量
nlink_t整型文件的(硬)连接数量(15.1 节)
off_t有符号整型文件偏移量或大小(4.7 节及 15.1 节)
pid_t有符号整型进程 ID、进程组 ID 或会话 ID(6.3 节、34.2 节、34.3 节)
ptrdiff_t有符号整型两指针差值,为有符号整型
rlim_t无符号整型资源限制(36.2 节)
sa_family_t无符号整型套接字地址族(56.4 节)
shmatt_t无符号整型与 System V 共享内存段相连的进程数量
sig_atomic_t整型可进行原子访问的数据类型(21.1.3 节)
siginfo_t结构类型信号起源的相关信息(21.4 节)
sigset_t整型或结构类型信号集合(20.9 节)
size_t无符号整型对象大小(以字节数计)
socklen_t至少 32 位的整型套接字地址结构大小(以字节数计)(56.3 节)
speed_t无符号整型终端线速度(62.7 节)
ssize_t有符号整型字节数或(为负时)标识错误
stack_t结构类型对备选信号栈的描述(21.3 节)
suseconds_t有符号整型,范围为 [-1,1000000]微秒级的时间间隔(10.1 节)
tcflag_t无符号整型终端模式标志位的位掩码(62.2 节)
time_t整型或浮点型实数自所谓纪元(Epoch)(10.1 节)始,以秒计的日历时间
timer_t运算类型之一POSIX.1b 间隔定时器函数(23.6 节)的定时器标识符
uid_t整型数值型用户标识符(8.1 节)

文件 I/O:通用的 I/O 模型

概述

所有执行 I/O 操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。针对每个进程,文件描述符都自成一套按照。惯例,大多数程序都期望能够使用 3 种标准的文件描述符,如下表

文件描述符用 途POSIX 名称stdio
0标准输入STDIN_FILENOstdin
1标准输出STDOUT_FILENOstdout
2标准错误STDERR_FILENOstder

I/O 操作的 4 个主要系统调用

1.int open(const char *pathname, int flags, mode_t mode);

2.ssize_t read(int fd, void *buffer, size_t count);

3.ssize_t write(int fd, const void *buffer, size_t count);

4.int close(int fd);

通用 I/O

UNIX I/O 模型的显著特点之一是其输入/输出的通用性概念。这意味着使用 4 个同样的系统调用 open()、read()、write()和 close()可以对所有类型的文件执行 I/O 操作,包括终端之类的设备。因此,仅使用这些系统调用编写的程序,将对任何类型的文件有效。

open()函数

open()调用既能打开一个业已存在的文件,也能创建并打开一个新文件。

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

int open(const char *pathname, int flags, mode_t mode);

文件访问模式标志

O_RDONLY,O_WRONLYO_RDWR这三种只能选择一种

标志用途统一 UNIX 规范版本
O_RDONLY以只读方式打开v3
O_WRONLY以只写方式打开v3
O_RDWR以读写方式打开v3
O_CLOEXEC设置 close-on-exec 标志(自 Linux 2.6.23 版本开始)v4
O_CREAT若文件不存在则创建之v3
O_DIRECT无缓冲的输入/输出
O_DIRECTORY如果 pathname 不是目录,则失败v4
O_EXCL结合 O_CREAT 参数使用,专门用于创建文件v3
O_LARGEFILE在 32 位系统中使用此标志打开大文件
O_NOATIME调用 read() 时,不修改文件最近访问时间(自 Linux 2.6.8 版本开始)
O_NOCTTY不要让 pathname(所指向的终端设备)成为控制终端v3
O_NOFOLLOW对符号链接不予解引用v4
O_TRUNC截断已有文件,使其长度为零v3
O_APPEND总在文件尾部追加数据v3
O_ASYNC当 I/O 操作可行时,产生信号(signal)通知进程
O_DSYNC提供同步的 I/O 数据完整性(自 Linux 2.6.33 版本开始)v3
O_NONBLOCK以非阻塞方式打开v3
O_SYNC以同步方式写入文件v3

open() 函数的常见错误

错误代码描述
EACCES无权限以指定方式打开文件,可能因目录权限限制、文件不存在且无法创建。
EISDIR尝试以写方式打开目录文件(不允许)。
EMFILE进程已打开文件描述符数量达到限制。
ENFILE系统文件打开数量达到上限。
ENOENT文件不存在且未指定 O_CREAT,或路径中目录不存在。
EROFS文件隶属只读文件系统,试图以写方式打开。
ETXTBSY文件为正在运行的可执行文件,系统禁止修改。

注意: 更多错误原因可查看 man 2 open 手册。

creat()函数

在早期的 UNIX 实现中,open()只有两个参数,无法创建新文件,而是使用 creat()系统调用来创建并打开一个新文件。

1
2
3
#include <fcntl.h>

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

creat()系统调用根据 pathname 参数创建并打开一个文件,若文件已存在,则打开文件,并清空文件内容,将其长度清 0。creat()返回一文件描述符,供后续系统调用使用。creat()系统调用等价于如下 open()调用:

1
fd =open(pathname,0_WRONLY | O_CREAT | O_TRUNC,mode);

read()函数

read()系统调用从文件描述符 fd 所指代的打开文件中读取数据。

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

ssizet read(int fd,void *buffer,sizet count);

// 返回读取的字节数,EOF(文件结束)时返回 0,出错时返回 -1

count 参数指定最多能读取的字节数。(size_t 数据类型属于无符号整数类型。)buffer 参数提供用来存放输入数据的内存缓冲区地址。缓冲区至少应有 count 个字节。

write()函数

write()系统调用将数据写入一个已打开的文件中。

1
2
3
4
5
#include <unistd,h>

ssize t write(int fd,void *buffer,sizet count);

// 返回写入的字节数,出错时返回 -1

write()调用的参数含义与 read()调用相类似。buffer 参数为要写入文件中数据的内存地址,count参数为欲从 buffer 写入文件的数据字节数,fd 参数为一文件描述符,指代数据要写入的文件。如果 write()调用成功,将返回实际写入文件的字节数,该返回值可能小于 count 参数值。这被称为“部分写”。对磁盘文件来说,造成“部分写”的原因可能是由于磁盘已满,或是因为进程资源对文件大小的限制。

close()函数

close()系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当一进程终止时,将自动关闭其已打开的所有文件描述符。

1
2
3
4
5
#include <unistd,h>

int close(int fd);

// 成功时返回 0,出错时返回 -1

lseek()函数

对于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个 read()write()操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。文件打开时,会将文件偏移量设置为指向文件开始,以后每次read()write()调用将自动对其进行调整,以指向已读或已写数据后的下一字节。因此,连续的 read()write()调用将按顺序递进,对文件进行操作。针对文件描述符 fd 参数所指代的已打开文件,lseek()系统调用依照 offsetwhence 参数值调整该文件的偏移量。

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

off t lseek(int fd,off t offset,int whence);

// 成功时返回新的文件偏移量,出错时返回 -1

offset 参数指定了一个以字节为单位的数值。(SUSv3 规定 off_t 数据类型为有符号整型数。)whence 参数则表明应参照哪个基点来解释 offset 参数,应为下列其中之一:

标志描述
SEEK_SET将文件偏移量设置为从文件头部起始点开始的 offset 个字节。
SEEK_CUR相对于当前文件偏移量,将文件偏移量调整 offset 个字节。
SEEK_END将文件偏移量设置为起始于文件尾部的 offset 个字节。offset 参数从文件最后一个字节之后算起。

深入探究文件 I/O

fcntl()函数

fcntl()系统调用对一个打开的文件描述符执行一系列控制操作。

1
2
3
4
#include <fcntl,h>
int fcntl(int fd,int cmd,...);

//成功时返回取决于cmd,错误时返回-1

cmd 参数所支持的操作范围很广,常用来获取/设置文件描述符标志,获取/设置文件状态标志,设置/获取文件锁,复制文件描述符。fcntl()的第三个参数以省略号来表示,这意味着可以将其设置为不同的类型,或者加以省略。内核会依据 cmd 参数(如果有的话)的值来确定该参数的数据类型。

使用示例(获取和设置文件描述符标志:`F_GETFD` 和 `F_SETFD`)

获取或设置文件描述符的标志,例如 FD_CLOEXEC(关闭文件时不被子进程继承)。

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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}

// 获取文件描述符标志
int flags = fcntl(fd, F_GETFD);
if (flags == -1) {
perror("fcntl F_GETFD");
close(fd);
return 1;
}
printf("Initial FD flags: %d\n", flags);

// 设置 FD_CLOEXEC 标志
flags |= FD_CLOEXEC;
if (fcntl(fd, F_SETFD, flags) == -1) {
perror("fcntl F_SETFD");
close(fd);
return 1;
}
printf("FD_CLOEXEC flag set.\n");

close(fd);
return 0;
}

使用示例(获取和设置文件状态标志:`F_GETFL` 和 `F_SETFL`)

获取或修改文件的状态标志,例如非阻塞模式(O_NONBLOCK)或追加模式(O_APPEND)。

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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) {
perror("open");
return 1;
}

// 获取文件状态标志
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL");
close(fd);
return 1;
}
printf("Initial file status flags: %d\n", flags);

// 添加非阻塞标志
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL");
close(fd);
return 1;
}
printf("O_NONBLOCK flag set.\n");

close(fd);
return 0;
}
使用示例(文件锁:`F_SETLK` 和`F_SETLKW`和 `F_GETLK`)

设置、获取或阻塞文件锁(适合实现进程间文件同步)。

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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}

struct flock lock;
lock.l_type = F_WRLCK; // 写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0; // 从文件开头
lock.l_len = 0; // 锁住整个文件

// 尝试设置锁
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("fcntl F_SETLK");
close(fd);
return 1;
}
printf("File locked.\n");

// 模拟文件操作
sleep(10);

// 释放锁
lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("fcntl F_SETLK");
close(fd);
return 1;
}
printf("File unlocked.\n");

close(fd);
return 0;
}
使用示例(复制文件描述符:`F_DUPFD` 和 `F_DUPFD_CLOEXEC`)

类似于 dup()dup3(),但支持更精细的控制。

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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("open");
return 1;
}

// 复制文件描述符,最小值为 5
int new_fd = fcntl(fd, F_DUPFD, 5);
if (new_fd == -1) {
perror("fcntl F_DUPFD");
close(fd);
return 1;
}
printf("New file descriptor: %d\n", new_fd);

write(new_fd, "Hello, fcntl!\n", 14);

close(fd);
close(new_fd);
return 0;
}

dup()函数

dup()调用复制一个打开的文件描述符 oldfd,并返回一个新描述符,二者都指向同一打开的文件句柄。系统会保证新描述符一定是编号值最低的未用文件描述符。

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

int dup(int oldfd);

// 成功时返回(新)文件描述符,错误时返回-1
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}

int new_fd = dup(fd);
if (new_fd == -1) {
perror("dup");
exit(EXIT_FAILURE);
}

write(new_fd, "Hello, dup!\n", 12);
close(fd);
close(new_fd);
exit(EXIT_SUCCESS);
}

运行完毕后文件 example.txt 会包含 Hello, dup!

dup2()函数

1
2
3
4
5
#include <unistd,h>

int dup2(int oldfd,int newfd);

// 成功时返回(新)文件描述符,错误时返回-1

dup2()系统调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。如果由 newfd 参数所指定编号的文件描述符之前已经打开,那么 dup2()会首先将其关闭。(dup2()调用会默然忽略 newfd 关闭期间出现的任何错误。故此,编码时更为安全的做法是:在调用dup2()之前,若 newfd 已经打开,则应显式调用 close()将其关闭。)如果 oldfdnewfd 相同,dup2() 直接返回 newfd,不会关闭或复制。

使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}

dup2(fd, STDOUT_FILENO); // 重定向标准输出到文件
printf("这将被写入文件而不是终端.\n");

close(fd);
return 0;
}

运行完毕后文件example.txt 会包含重定向后的输出内容"这将被写入文件而不是终端."

dup3()函数

dup3()系统调用完成的工作与 dup2()相同,只是新增了一个附加参数 flag,这是一个可以修改系统调用行为的位掩码。

1
2
3
4
5
6
7
#define GNU SOURCE

#include <unistd.h>

int dup3(int oldfd,int newfd,int flags);

// 成功时返回(新)文件描述符,错误时返回-1

目前,dup3()只支持一个标志 O_CLOEXEC,这将促使内核为新文件描述符设置 close-on-exec标志(FD_CLOEXEC)。

使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}

dup3(fd, 5, O_CLOEXEC); // 复制到文件描述符 5,并设置 O_CLOEXEC
write(5, "Hello, dup3!\n", 13);

close(fd);
close(5);
return 0;
}

运行完毕后文件example.txt 会包含 Hello, dup3!

pread()函数

系统调用 pread()完成与 read()相类似的工作,只是前者会在 offset 参数所指定的位置进行文件 I/O 操作,而非始于文件的当前偏移量处,且它们不会改变文件的当前偏移量。

1
2
3
4
5
#include <unistd,h>

ssize_t pread(int fd,void *buf,size_t count,off_t offset);

// 返回读取的字节数,EOF(文件结束)时返回 0,发生错误时返回 -1。

pread()系统调用相当于把下面的操作,纳入了原子操作

1
2
3
4
5
6
7
8
9
10
11
12
off_t = orig;
ssize_t t;
char buf[1024]; // 定义一个大小为 1024 字节的缓冲区
// 获取当前文件描述符 fd 的文件偏移量,并将其保存到 orig 中,SEEK_CUR 表示相对于当前偏移量的位置。
orig = lseek(fd,0,SEEK_CUR);
// 将文件偏移量移动到指定的 offset 位置。SEEK_SET 表示从文件的开头计算偏移量。
lseek(fd,offset,SEEK_SET);
ssize_t read(fd, buf, len);
// 从文件描述符 fd 处读取最多 len 字节的数据到缓冲区 buf 中,返回实际读取的字节数。
s = read(fd,buf,len);
// 将文件偏移量恢复到之前保存的 orig 值。这样,文件的读取或写入位置不会因为这段操作而改变。
1seek(fd,orig,SEEK_SET);
使用示例
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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd;
char buf[20];
ssize_t bytesRead;

// 打开文件
fd = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 从偏移量 10 开始读取 15 个字节
bytesRead = pread(fd, buf, 15, 10);
if (bytesRead < 0) {
perror("pread failed");
close(fd);
exit(EXIT_FAILURE);
}

// 添加字符串结束符
buf[bytesRead] = '\0';

// 打印读取的数据
printf("Data read: %s\n", buf);

// 关闭文件
close(fd);
return 0;
}

example.txt的文件内容

1
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ

运行上述程序会输出:

1
Data read: ABCDEFGHIJKLM

pwrite()函数

系统调用 pwrite()完成与 write()相类似的工作,只是前者会在 offset 参数所指定的位置进行文件 I/O 操作,而非始于文件的当前偏移量处,且它们不会改变文件的当前偏移量。

1
2
3
4
5
#include <unistd,h>

ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

// 返回写入的字节数,EOF(文件结束)时返回 0,发生错误时返回 -1。
使用示例
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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
int fd;
const char *data = "Hello, pwrite!";
ssize_t bytesWritten;

// 打开文件(如果文件不存在则创建)
fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd < 0) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 从偏移量 5 开始写入数据
bytesWritten = pwrite(fd, data, strlen(data), 5);
if (bytesWritten < 0) {
perror("pwrite failed");
close(fd);
exit(EXIT_FAILURE);
}

printf("Bytes written: %zd\n", bytesWritten);

// 关闭文件
close(fd);
return 0;
}

example.txt的文件内容

1
1234567890

运行程序后,example.txt 内容变为:

1
12345Hello, pwrite!

readv()函数

readv() 是一种 POSIX 系统调用,用于从文件描述符中读取数据到多个缓冲区。它的主要功能是将数据按顺序填充到指定的多个缓冲区中,而不是将数据集中到一个缓冲区中。这样可以减少内存拷贝,提高性能。

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

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

// 成功返回读取的总字节数,即所有缓冲区中实际填充的字节数的总和。返回 -1,并设置全局变量 errno 表示错误原因。

这些系统调用并非只对单个缓冲区进行读写操作,而是一次即可传输多个缓冲区的数据。数组 iov 定义了一组用来传输数据的缓冲区。整型数 iovcnt 则指定了 iov 的成员个数。iov 中的每个成员都是如下形式的数据结构。

1
2
3
4
5
6
#include <sys/uio.h>

struct iovec {
void *iov_base; // 指向缓冲区的起始地址
size_t iov_len; // 缓冲区的长度(以字节为单位)
};
使用示例
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
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
int fd;
ssize_t bytesRead;
char buf1[10];
char buf2[20];

// 打开文件
fd = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 定义两个 iovec 缓冲区
struct iovec iov[2];
iov[0].iov_base = buf1; // 第一个缓冲区
iov[0].iov_len = sizeof(buf1); // 第一个缓冲区大小

iov[1].iov_base = buf2; // 第二个缓冲区
iov[1].iov_len = sizeof(buf2); // 第二个缓冲区大小

// 从文件中读取数据到 iov 中的缓冲区
bytesRead = readv(fd, iov, 2);
if (bytesRead < 0) {
perror("readv failed");
close(fd);
exit(EXIT_FAILURE);
}

// 打印读取到的内容
buf1[iov[0].iov_len - 1] = '\0'; // 确保缓冲区末尾有 '\0'
buf2[bytesRead - sizeof(buf1) - 1] = '\0';
printf("Buffer 1: %s\n", buf1);
printf("Buffer 2: %s\n", buf2);

// 关闭文件
close(fd);
return 0;
}

example.txt的文件内容

1
HelloWorldThisIsReadvExample

运行程序后,输出为:

1
2
Buffer 1: HelloWorl
Buffer 2: dThisIsReadvEx

writev()函数

writev() 是一个系统调用,允许程序将多个非连续缓冲区的数据写入到文件描述符中。它可以一次性地将分散的多个缓冲区内容输出到目标文件(或套接字)中,而不需要手动合并数据,从而提高性能。

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

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

// 成功:返回实际写入的字节数。失败:返回 -1 并设置 errno 以指示错误。

writev()系统调用实现了集中输出:将 iov 所指定的所有缓冲区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符 fd 指代的文件中。对缓冲区中数据的“集中”始于iov[0]所指定的缓冲区,并按数组顺序展开。像 readv()调用一样,writev()调用也属于原子操作,即所有数据将一次性地从用户内存传输到 fd 指代的文件中。因此,在向普通文件写入数据时,writev()调用会把所有的请求数据连续写入文件,而不会在其他进程(或线程)写操作的影响下1 分散地写入文件2 。如同 write()调用,writev()调用也可能存在部分写的问题。因此,必须检查 writev()调用的返回值,以确定写入的字节数是否与要求相符。

使用示例
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
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd;
ssize_t bytesWritten;

// 打开文件以写入
fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 定义两个缓冲区
const char *buf1 = "Hello, ";
const char *buf2 = "World!\n";

// 设置 iovec 数组
struct iovec iov[2];
// void * 是 C 和 C++ 语言中一种通用的指针类型,表示 "指向未知类型的指针"。
iov[0].iov_base = (void *)buf1; // 第一个缓冲区
iov[0].iov_len = strlen(buf1); // 第一个缓冲区的长度

iov[1].iov_base = (void *)buf2; // 第二个缓冲区
iov[1].iov_len = strlen(buf2); // 第二个缓冲区的长度

// 使用 writev 写入文件
bytesWritten = writev(fd, iov, 2);
if (bytesWritten < 0) {
perror("writev failed");
close(fd);
exit(EXIT_FAILURE);
}

printf("Successfully written %zd bytes.\n", bytesWritten);

// 关闭文件
close(fd);
return 0;
}

运行程序后,example.txt文件内容为:

1
Hello, World!

preadv()函数

preadv() 是 Linux 和 Unix 系统中的系统调用,用于 从文件描述符的指定偏移量读取数据,并存储到多个缓冲区中。与 readv() 不同,preadv() 的一个显著特性是它是一个 “原子” 操作,它结合了 readv()lseek() 的功能,同时不会更改文件描述符的文件偏移量。

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

ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, off_t offset);

// 成功:返回实际读取的字节数。失败:返回 -1,并设置 errno 指定错误原因。
使用示例
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
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
int fd;
ssize_t bytesRead;
struct iovec iov[2];
char buf1[10], buf2[20];
off_t offset = 5;

// 打开文件
fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 设置 iovec 缓冲区
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);

// 使用 preadv 从偏移量读取数据
bytesRead = preadv(fd, iov, 2, offset);
if (bytesRead == -1) {
perror("preadv failed");
close(fd);
exit(EXIT_FAILURE);
}

printf("Bytes read: %zd\n", bytesRead);
printf("Buffer 1: %.*s\n", (int)iov[0].iov_len, buf1);
printf("Buffer 2: %.*s\n", (int)(bytesRead - iov[0].iov_len), buf2);

close(fd);
return 0;
}

文件 example.txt 的内容是:

1
HelloWorldThisIspreadvExample

运行程序后,输出结果为

1
2
Buffer 1: WorldThis
Buffer 2: IspreadvExampl

pwritev()函数

pwritev() 是 Linux 和 Unix 系统中的系统调用,用于 从多个缓冲区将数据写入文件的指定偏移量。与 writev() 的区别在于,pwritev() 提供了一个显式的偏移量参数,写操作从文件中的指定位置开始,而不会更改文件描述符的当前偏移量。

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

ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset);

// 成功:返回实际写入的字节数。失败:返回 -1,并设置 errno 指定错误原因。
使用示例
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
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
int fd;
ssize_t bytesWritten;
struct iovec iov[2];
char buf1[] = "Hello ";
char buf2[] = "World!";

// 打开文件
fd = open("example.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 设置 iovec 缓冲区
iov[0].iov_base = buf1;
iov[0].iov_len = strlen(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = strlen(buf2);

// 使用 pwritev 从偏移量 0 开始写入数据
bytesWritten = pwritev(fd, iov, 2, 0);
if (bytesWritten == -1) {
perror("pwritev failed");
close(fd);
exit(EXIT_FAILURE);
}

printf("Bytes written: %zd\n", bytesWritten);

// 关闭文件
close(fd);
return 0;
}

运行程序后,example.txt文件内容为

1
Hello World!

输出结果:

1
Bytes written: 12

truncate() 函数

truncate() 是一个文件操作系统调用,用于将指定文件的大小调整为指定值。如果文件变小,超出部分将被截断;如果文件变大,扩展部分通常填充为零。

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

int truncate(const char *path, off_t length);

//成功:返回 0。失败:返回 -1,并设置 errno 指定错误原因。

path:表示文件路径,指向需要调整大小的文件。

length:新的文件大小(以字节为单位)。若文件当前大小大于 length,多余部分会被删除。若文件当前大小小于 length,文件会被扩展,新增部分通常填充为零(文件系统依赖)。

使用示例(截断文件)
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
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const char *filePath = "example.txt";
off_t newSize = 5; // 截断到 5 字节

// 打开或创建文件并写入内容
int fd = open(filePath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}
write(fd, "Hello, World!", 13);
close(fd);

// 截断文件
if (truncate(filePath, newSize) == -1) {
perror("truncate failed");
exit(EXIT_FAILURE);
}

printf("File '%s' has been truncated to %ld bytes.\n", filePath, (long)newSize);

return 0;
}

运行程序后,example.txt文件内容为

1
Hello
使用示例(扩展文件大小)
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
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const char *filePath = "example.txt";
off_t newSize = 20; // 扩展到 20 字节

// 创建并写入初始内容
int fd = open(filePath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}
write(fd, "Hello", 5);
close(fd);

// 扩展文件大小
if (truncate(filePath, newSize) == -1) {
perror("truncate failed");
exit(EXIT_FAILURE);
}

printf("File '%s' has been extended to %ld bytes.\n", filePath, (long)newSize);

return 0;
}

运行程序后,example.txt文件内容为

1
Hello\0\0\0\0\0\0\0\0\0\0\0\0\0

ftruncate()函数

ftruncate() 是一个文件操作系统调用,用于通过文件描述符调整打开文件的大小。如果文件变小,超出部分会被截断;如果文件变大,扩展部分通常填充为零。

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

int ftruncate(int fd, off_t length);

// 成功:返回 0。失败:返回 -1,并设置 errno 以指示错误原因。

fd:打开文件的文件描述符。文件描述符必须是可写的。

length:调整后的文件大小(以字节为单位)。若文件当前大小大于 length,多余部分会被删除。若文件当前大小小于 length,文件会被扩展,新增部分通常填充为零(文件系统依赖)。

使用示例(截断文件)
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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const char *filePath = "example.txt";
off_t newSize = 5; // 将文件大小调整为 5 字节

// 打开文件
int fd = open(filePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 写入初始内容
write(fd, "Hello, World!", 13);

// 调整文件大小
if (ftruncate(fd, newSize) == -1) {
perror("ftruncate failed");
close(fd);
exit(EXIT_FAILURE);
}

printf("File '%s' truncated to %ld bytes.\n", filePath, (long)newSize);

// 关闭文件
close(fd);
return 0;
}

运行程序后,example.txt文件内容为

1
Hello
使用示例(扩展文件大小)
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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
const char *filePath = "example.txt";
off_t newSize = 20; // 将文件大小扩展为 20 字节

// 打开文件
int fd = open(filePath, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("Failed to open file");
exit(EXIT_FAILURE);
}

// 写入初始内容
write(fd, "Hello", 5);

// 调整文件大小
if (ftruncate(fd, newSize) == -1) {
perror("ftruncate failed");
close(fd);
exit(EXIT_FAILURE);
}

printf("File '%s' extended to %ld bytes.\n", filePath, (long)newSize);

// 关闭文件
close(fd);
return 0;
}

运行程序后,example.txt文件内容为

1
Hello\0\0\0\0\0\0\0\0\0\0\0\0\0

mkstemp()函数

mkstemp() 是一个用于创建临时文件的系统调用。它会创建一个唯一命名的临时文件,防止文件名冲突,并返回一个文件描述符,用于后续文件操作。

1
2
3
4
5
#include <stdlib.h>

int mkstemp(char *template);

// 成功:返回一个打开的文件描述符(以读写模式打开)。失败:返回 -1,并设置 errno 以指示错误原因。

template:是一个指向以 NULL 结尾的字符串的指针。此字符串必须以 六个连续的 X 结尾(如 "tempXXXXXX")。mkstemp() 会用唯一的文件名替换这些 X,生成文件。注意:该字符串会被修改,最终包含生成的文件名。

使用示例(创建临时文件)
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
char template[] = "/tmp/mytempXXXXXX"; // 模板必须以 6 个 X 结尾
int fd = mkstemp(template);

if (fd == -1) {
perror("mkstemp failed");
exit(EXIT_FAILURE);
}

printf("Temporary file created: %s\n", template);

// 写入数据
write(fd, "Hello, World!\n", 14);

// 关闭并删除文件
close(fd);
// 调用 unlink() 删除临时文件。
unlink(template);

return 0;
}
使用示例(在指定目录中创建临时文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
char template[] = "./tempXXXXXX"; // 在当前目录创建临时文件
int fd = mkstemp(template);

if (fd == -1) {
perror("mkstemp failed");
exit(EXIT_FAILURE);
}

printf("Temporary file created: %s\n", template);

// 不删除文件,仅关闭描述符
close(fd);

return 0;
}

注意事项

安全性

  • 避免使用不安全的函数(如 tmpnam()),mkstemp() 能更好地防止文件名冲突和竞态条件。

目录权限

  • 如果指定路径中的目录不可写,mkstemp() 会失败。

文件名限制

  • 模板中的 X 必须不少于 6 个,否则 mkstemp() 会报错。

文件清理

  • 创建的临时文件不会自动删除,开发者需要在不使用时调用 unlink() 手动删除文件。

tmpfile()函数

tmpfile() 是一个标准 C 库函数,用于创建临时文件。它会创建一个匿名的临时文件,该文件在关闭时或程序退出时会自动删除。

1
2
3
4
5
#include <stdio.h>

FILE *tmpfile(void);

// 成功:返回指向临时文件的文件指针 (FILE *)。失败:返回 NULL,并设置 errno 表示错误原因。

tmpfile()函数执行成功,将返回一个文件流供 stdio 库函数使用。文件流关闭后将自动删除临时文件。为达到这一目的,tmpfile()函数会在打开文件后,从内部立即调用 unlink()来删除该文件名 。

使用示例(创建并写入临时文件)
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
#include <stdio.h>
#include <stdlib.h>

int main() {
FILE *fp = tmpfile();
if (fp == NULL) {
perror("tmpfile failed");
exit(EXIT_FAILURE);
}

// 写入数据到临时文件
fprintf(fp, "This is a temporary file.\n");
fprintf(fp, "It will be automatically deleted.\n");

// 将文件指针重置到文件开头
rewind(fp);

// 读取并打印数据
char buffer[128];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}

// 关闭临时文件(自动删除)
fclose(fp);

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
#include <stdio.h>
#include <stdlib.h>

int main() {
FILE *tempFile = tmpfile();
if (tempFile == NULL) {
perror("tmpfile failed");
exit(EXIT_FAILURE);
}

int data[] = {10, 20, 30, 40, 50};
size_t count = sizeof(data) / sizeof(data[0]);

// 写入数据到临时文件
fwrite(data, sizeof(int), count, tempFile);

// 将文件指针重置到文件开头
rewind(tempFile);

// 读取数据并打印
int value;
while (fread(&value, sizeof(int), 1, tempFile) == 1) {
printf("%d\n", value);
}

// 关闭临时文件
fclose(tempFile);

return 0;
}

进程

getpid()函数

getpid() 是一个标准的系统调用,用于获取当前进程的进程标识符(PID)。

每个进程都有一个进程号(PID),进程号是一个正数,用以唯一标识系统中的某个进程。对各种系统调用而言,进程号有时可以作为传入参数,有时可以作为返回值。比如,系统调用kill()允许调用者向拥有特定进程号的进程发送一个信号。当需要创建一个对某进程而言唯一的标识符时,进程号就会派上用场。常见的例子是将进程号作为与进程相关文件名的一部分。

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

pid_t getpid(void);

// 返回当前进程的 ID (pid_t 类型)。通常,进程 ID 是一个大于零的整数。

getpid()返回值的数据类型为 pid_t,该类型是由 SUSv3 所规定的整数类型,专用于存储进程号。除了少数系统进程外,比如 init 进程(进程号为 1),程序与运行该程序进程的进程号之间没有固定关系。Linux 内核限制进程号需小于等于 32767。新进程创建时,内核会按顺序将下一个可用的进程号分配给其使用。每当进程号达到 32767 的限制时,内核将重置进程号计数器,以便从小整数开始分配。

使用示例
1
2
3
4
5
6
7
8
#include <stdio.h>
#include <unistd.h>

int main() {
pid_t pid = getpid();
printf("Current Process ID: %d\n", pid);
return 0;
}

getppid()函数

getppid() 是一个系统调用,用于获取当前进程的父进程标识符(Parent Process ID, PPID)。

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

pid_t getppid(void);

// 返回当前进程的父进程的 ID (pid_t 类型)。通常是一个非负整数。特殊情况:当父进程被杀死后,当前进程的父进程会变为系统的 init 进程或其现代替代进程(如 systemd),这时返回的 PPID 为 1。

实际上,每个进程的父进程号属性反映了系统上所有进程间的树状关系。每个进程的父进程又有自己的父进程,以此类推,回溯到 1 号进程—init 进程,即所有进程的始祖。使用pstree(1)命令可以查看到这一“家族树”(family tree)。如果子进程的父进程终止,则子进程就会变成“孤儿”,init 进程随即将收养该进程,子进程后续对 getppid()的调用将返回进程号 1。通过查看由 Linux 系统所特有的/proc/PID/status 文件所提供的 PPid 字段,可以获知每个进程的父进程。

使用示例
1
2
3
4
5
6
7
8
#include <stdio.h>
#include <unistd.h>

int main() {
pid_t ppid = getppid();
printf("Parent Process ID: %d\n", ppid);
return 0;
}

getenv()函数

getenv()函数能够从进程环境中检索单个值。

1
2
3
4
5
#include <stdlib.h>

char *getenv(const char *name);

// 成功: 返回一个指向环境变量值的指针。如果环境变量存在,则返回其值的字符串指针。返回的值是指向环境变量中存储数据的内存,不要修改此内存的内容。失败: 返回 NULL,表示未找到指定的环境变量。

name: 环境变量的名称,必须是一个以空字符 '\0' 结尾的字符串。

功能说明:getenv() 检索环境变量的值。环境变量 是键值对形式的字符串,通常用于存储配置信息,如用户路径、系统设置等。如果环境变量存在,则返回指向该变量值的指针;如果不存在,则返回 NULL

使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h>
#include <stdio.h>

int main() {
// 获取 PATH 环境变量
const char *path = getenv("PATH");
if (path == NULL) {
printf("Environment variable PATH not found.\n");
} else {
printf("PATH: %s\n", path);
}

// 获取不存在的环境变量
const char *notExist = getenv("NOT_EXIST");
if (notExist == NULL) {
printf("Environment variable NOT_EXIST not found.\n");
}

return 0;
}

输出内容

1
2
PATH: /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
Environment variable NOT_EXIST not found.

注意事项

  1. 检查返回值: 环境变量可能不存在,使用前需检查返回值是否为 NULL

  2. 多线程使用: 在多线程程序中避免直接调用 getenv(),需要同步或使用线程安全的替代方法。

  3. 与环境变量的关系: 环境变量由系统和用户定义,通常可以通过 export 命令设置。例如:

    1
    export MYVAR="HelloWorld"

    然后可以在 C 程序中使用 getenv("MYVAR") 获取其值。

putenv()函数

putenv() 是一个标准 C 库函数,用于向环境中添加新的环境变量或修改已有的环境变量。

1
2
3
4
5
#include <stdlib.h>

int putenv(char *string);

// 返回值0: 成功。-1: 失败(例如,内存不足时)。

string:一个以 name=value 格式表示的字符串,用于设置环境变量。name 是环境变量的名称,value 是对应的值。必须确保传递的字符串格式正确,且以 \0 结尾。

使用示例(添加新变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h>
#include <stdio.h>

int main() {
// 添加环境变量
if (putenv("MYVAR=HelloWorld") != 0) {
perror("putenv failed");
return EXIT_FAILURE;
}

// 验证变量是否已添加
const char *value = getenv("MYVAR");
if (value == NULL) {
printf("Environment variable MYVAR not found.\n");
} else {
printf("MYVAR: %s\n", value);
}

return EXIT_SUCCESS;
}
使用示例(修改已有变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main() {
// 设置环境变量
char env[] = "MYVAR=InitialValue";
if (putenv(env) != 0) {
perror("putenv failed");
return EXIT_FAILURE;
}

printf("Before modification: %s\n", getenv("MYVAR"));

// 修改变量的值
strcpy(env, "MYVAR=ModifiedValue"); // 修改内存内容
printf("After modification: %s\n", getenv("MYVAR"));

return EXIT_SUCCESS;
}

注意事项

  1. string 的内存管理
    • putenv() 不复制传递的字符串,直接使用它的指针。
    • 如果 string 是动态分配的,释放该内存可能导致环境变量变为非法。
  2. 修改内容的副作用
    • 修改传递给 putenv() 的字符串内容会直接影响环境变量。
    • 示例中,通过 strcpy 修改 env 后,getenv() 的返回值也会随之改变。
  3. 格式要求
    • 必须以 name=value 格式传递字符串。
    • 如果格式不正确,可能会导致不可预期的行为。
  4. 线程安全性
    • putenv()非线程安全 的。
    • 在多线程程序中调用时,需确保同步机制,避免竞争条件。
  5. 性能问题
    • 每次调用 putenv() 都可能导致环境变量表重新分配内存,影响性能。
  6. 建议使用 setenv() 替代
    • setenv() 是线程安全的,并且会复制传入的值,避免了许多 putenv() 的问题。

setenv()函数

setenv() 是一个标准 C 库函数,用于添加新的环境变量或修改现有的环境变量。

1
2
3
4
5
#include <stdlib.h>

int setenv(const char *name, const char *value, int overwrite);

// 0: 成功。-1: 失败(例如,内存不足或参数无效)。错误时,errno 会被设置为具体错误码。

name:环境变量的名称。必须是有效的字符串,不能包含等号 (=),且不能为空。

value:环境变量的值。如果为空字符串,表示设置该变量为空值。

overwrite:一个整型标志,用于指示是否允许覆盖已有的环境变量:非零值:如果环境变量已存在,将覆盖其值。零值:如果环境变量已存在,则不改变其值。

使用示例(添加新环境变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdlib.h>
#include <stdio.h>

int main() {
// 添加新的环境变量
if (setenv("MYVAR", "HelloWorld", 1) != 0) {
perror("setenv failed");
return EXIT_FAILURE;
}

// 验证是否添加成功
const char *value = getenv("MYVAR");
if (value) {
printf("MYVAR: %s\n", value);
} else {
printf("Environment variable MYVAR not found.\n");
}

return EXIT_SUCCESS;
}
使用示例(覆盖已有变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>

int main() {
// 设置环境变量
setenv("MYVAR", "InitialValue", 1);
printf("MYVAR before overwrite: %s\n", getenv("MYVAR"));

// 修改已有变量
setenv("MYVAR", "ModifiedValue", 1); // 允许覆盖
printf("MYVAR after overwrite: %s\n", getenv("MYVAR"));

return EXIT_SUCCESS;
}
使用示例(不覆盖已有变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <stdio.h>

int main() {
// 设置环境变量
setenv("MYVAR", "InitialValue", 1);
printf("MYVAR before overwrite: %s\n", getenv("MYVAR"));

// 尝试不覆盖已有变量
setenv("MYVAR", "IgnoredValue", 0); // 禁止覆盖
printf("MYVAR after no-overwrite attempt: %s\n", getenv("MYVAR"));

return EXIT_SUCCESS;
}

注意事项

  1. 参数验证
    • name 不能为空,且不能包含等号 (=)。
    • 若传递非法参数,setenv() 将返回错误并设置 errno
  2. 线程安全
    • 多线程环境下,setenv() 是安全的。
    • 但需要注意并发操作中对同一环境变量的竞争条件。
  3. 内存分配
    • setenv() 会分配内存来存储环境变量,用户无需关心内存管理。
    • 但频繁设置环境变量可能导致内存碎片化。
  4. 性能开销
    • setenv() 每次调用可能需要重新调整环境表,频繁调用可能影响性能。
  5. putenv() 的区别
    • setenv() 复制输入字符串,putenv() 不会复制,直接使用指针。
    • setenv() 更安全,但开销可能略高于 putenv()
  6. unsetenv() 配合使用
    • 可以使用 unsetenv() 删除环境变量,确保环境表保持干净。

进程的创建

fork()函数

在诸多应用中,创建多个进程是任务分解时行之有效的方法。例如,某一网络服务器进程可在侦听客户端请求的同时,为处理每一请求而创建一新的子进程,与此同时,服务器进程会继续侦听更多的客户端连接请求。以此类手法分解任务,通常会简化应用程序的设计,同时提高了系统的并发性。(即,可同时处理更多的任务或请求。)系统调用 fork()创建一新进程(child),几近于对调用进程(parent)的翻版。

1
2
3
4
5
6
#include <unistd.h>

pid t fork(void);

//在父进程中:成功时返回子进程的进程 ID,出错时返回 -1;
//在成功创建的子进程中:始终返回 0。

理解 fork()的诀窍是,要意识到,完成对其调用后将存在两个进程,且每个进程都会从 fork()的返回处继续执行。这两个进程将执行相同的程序文本段,但却各自拥有不同的栈段、数据段以及堆段拷贝。子进程的栈、数据以及栈段开始时是对父进程内存相应各部分的完全复制。执行 fork()之后,每个进程均可修改各自的栈数据、以及堆段中的变量,而并不影响另一进程

程序代码则可通过 fork()的返回值来区分父、子进程。在父进程中,fork()将返回新创建子进程的进程 ID。鉴于父进程可能需要创建,进而追踪多个子进程(通过 wait()或类似方法),这种安排还是很实用的。而 fork()在子进程中则返回 0。如有必要,子进程可调用 getpid()以获取自身的进程 ID,调用 getppid()以获取父进程 ID。

当无法创建子进程时,fork()将返回-1。失败的原因可能在于,进程数量要么超出了系统针对此真实用户(real user ID)在进程数量上所施加的限制,要么是触及允许该系统创建的最大进程数这一系统级上限。

调用 fork()时,有时会采用如下习惯用语:

1
2
3
4
5
6
7
8
9
10
11
12
13
pid_t childPid;
switch (childPid = fork())
{
case -1:
//处理错误
break;
case 0:
//子进程行为
break;
default:
//父进程行为
break;
}

调用 fork()之后,系统将率先“垂青”于哪个进程(即调度其使用 CPU),是无法确定的,意识到这一点极为重要。在设计拙劣的程序中,这种不确定性可能会导致所谓“竞争条件(race condition)”的错误

vfork()函数

在早期的 BSD 实现中,fork()会对父进程的数据段、堆和栈施行严格的复制。如前所述,这是一种浪费,尤其是在调用 fork()后立即执行 exec()的情况下。出于这一原因,BSD 的后期版本引入了 vfork()系统调用,尽管其运作含义稍微有些不同(实则有些怪异),但效率要远高于 BSD fork()。现代 UNIX 采用写时复制技术来实现 fork(),其效率较之于早期的 fork()实现要高出许多,进而将对 vfork()的需求剔除殆尽。虽然如此,Linux(如同许多其他的 UNIX 实现一样)还是提供了具有 BSD 语义的 vfork()系统调用,以期为程序提供尽可能快的 fork 功能。不过,鉴于 vfork()的怪异语义可能会导致一些难以察觉的程序缺陷(bug),除非能给性能带来重大提升(这种情况发生的概率极小),否则应当尽量避免使用这一调用。

类似于 fork(),vfork()可以为调用进程创建一个新的子进程。然而,vfork()是为子进程立即执行 exec()的程序而专门设计的。

1
2
3
4
5
6
#include <unistd,h>

pid t vfork(void);

//在父进程中:成功时返回子进程的进程 ID,失败时返回 -1;
//在成功创建的子进程中:始终返回 0。

vfork()因为如下两个特性而更具效率,这也是其与 fork()的区别所在。

  • 无需为子进程复制虚拟内存页或页表。相反,子进程共享父进程的内存,直至其成功执行了 exec()或是调用_exit()退出。
  • 在子进程调用 exec()或_exit()之前,将暂停执行父进程。

_exit()函数

通常,进程有两种终止方式。其一为异常(abnormal)终止,由对一信号的接收而引发,该信号的默认动作为终止当前进程,可能产生核心转储(core dump)。此外,进程可使用_exit()系统调用正常(normally)终止。

1
2
3
#include <unistd,h>

void exit(int status);

_exit()的 status 参数定义了进程的终止状态(termination status),父进程可调用 wait()以获取该状态。虽然将其定义为 int 类型,但仅有低 8 位可为父进程所用。按照惯例,终止状态为 0 表示进程“功成身退”,而非 0 值则表示进程因异常而退出。对非 0 返回值的解释则并无定例;不同的应用程序自成一派,并会在文档中加以描述。SUSv3 规定有两个常量:EXIT_SUCCESS(0)和 EXIT_FAILURE(1),本书中大部分程序就采用了这一约定。

调用_exit()的程序总会成功终止(即,_exit()从不返回)。

wait()函数

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

pid t wait(int *stalus);

//返回已终止子进程的进程 ID,或者在出错时返回 -1。