Linux 上常用的标准 IO 函数

Posted by KalosAner on January 23, 2025

一、引言

writeread 是通用的系统调用;recvsend 是专为网络通信设计的扩展接口,数据系统调用,但是用户可以对其进行封装;freadfwrite 是标准 IO 库函数。

二、系统调用

系统调用在调用时会通过内核缓冲区对数据进行缓冲。

延迟写入

当程序调用 write 函数时,数据并不会直接写入磁盘,而是暂存内核缓冲区,批量地写入磁盘。这样多次小文件写入会被合并为单次大块磁盘操作,减少磁头寻道时间。

预读

当程序调用 read 函数时,内核会预测程序的访问模式(如顺序读取),提前加载后续数据到缓冲区。例如读取第一块数据时,内核可能会预读第二、三块数据,后续访问可以直接从内存获取,避免多次磁盘寻址。

syscpy.c

三、标准 IO 函数

标准 IO 函数具有良好的移植性,并且标准 IO 除了可以使用内核缓冲区进行缓冲之外,还可以利用用户层的标准缓冲区提高性能。常用的标准 IO 函数有:freadfwritefputfdopenfileno

函数 作用 输入类型 输出类型
fwrite 写入数据块到流 数组指针、流指针 成功写入个数
fread 从流读取数据块 数组指针、流指针 成功读取个数
fputs 写入字符串到流 字符串指针、流指针 状态码
fdopen 将文件描述符转换为流指针 文件描述符 文件流指针
fileno 获取流的文件描述符 文件流指针 文件描述符
fopen 打开文件流 字符串指针 文件流指针
fgets 按行读取文本 文件流指针 状态码

函数原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
// ptr:需要写入的数据的指针,size:每个元素的大小,nmemb:要写入的元素个数,stream:输出流指针
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
// buffer:接收数据的指针,size:每个元素的大小,count:要读的元素个数,stream:输入流指针
int fputs(const char *str, FILE *stream);
//str:以空字符结尾的数据指针指向需要发送的数据,stream:输出流指针
char *fgets(char *str, int n, FILE *stream);
//str:以空字符结尾的数据指针用来接收数据,n:最多读取 n-1 个字符,stream:输入流指针
FILE *fopen(const char *filename, const char *mode);
//filename:文件路径名,mode:流模式
FILE *fdopen(int fildes, const char *mode);
//fildes:已打开的文件描述符,mode:流模式(如 "r", "w+"),需要与文件描述符的原始模式一致
int fileno(FILE *stream);
//stream:文件流指针

标准 IO 函数通过合并多次小数据写入为一次系统调用,从而减少用户态与内核态的切换开销,提升效率。

stdcpy.c

标准 IO 的缺点:不容易进行双向通信,有时需要频繁调用 fflush 函数,需要以 FILE 结构体指针的形式返回文件描述符。

四、IO 流分离

流指的是“数据流动”,可以比喻为数据收发的一种桥梁。

流分离就是把输入流和输出流分离,这样可以降低代码实现难度并且提高缓冲性能。

使用标准 IO 函数进行网络通信。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	FILE * readfp;
	FILE * writefp;
	
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t clnt_adr_sz;
	char buf[BUF_SIZE]={0,};

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));
	listen(serv_sock, 5);
	clnt_adr_sz=sizeof(clnt_adr); 
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
	
	readfp=fdopen(clnt_sock, "r");
	writefp=fdopen(clnt_sock, "w");
	
	fputs("FROM SERVER: Hi~ client? \n", writefp);
	fputs("I love all of the world \n", writefp);
	fputs("You are awesome! \n", writefp);
	fflush(writefp);
	
	fclose(writefp);	
	fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); 
	fclose(readfp);
	return 0;
}

如上是服务端代码,尝试进行半关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
	int sock;
	char buf[BUF_SIZE];
	struct sockaddr_in serv_addr;

	FILE * readfp;
	FILE * writefp;
	
	sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));
  
	connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	readfp=fdopen(sock, "r");
	writefp=fdopen(sock, "w");
  
	while(1)
	{
		if(fgets(buf, sizeof(buf), readfp)==NULL) 
			break;
		fputs(buf, stdout);
		fflush(stdout);
	 }  

	fputs("FROM CLIENT: Thank you! \n", writefp);
	fflush(writefp);
	fclose(writefp); fclose(readfp);
	return 0;
}

如上是客户端代码,当服务端尝试半关闭后继续向服务端发送数据。

通过运行得知,服务端无法收到客户端第 36 行代码发送的数据,这意味着服务端尝试半关闭时会关闭整个套接字。

为了实现半关闭可以使用 dup 函数对文件描述符进行复制,然后分别使用读和写进行打开得到读和写的文件流。半关闭时使用 shutdown 函数对文件流指针指向的文件描述符(这样即使复制再多的文件描述符,套接字也会进入半关闭状态)。

服务端代码如下(客户端代码无需更改):

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	FILE * readfp;
	FILE * writefp;
	
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t clnt_adr_sz;
	char buf[BUF_SIZE]={0,};

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr));
	listen(serv_sock, 5);
	clnt_adr_sz=sizeof(clnt_adr); 
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
	
	readfp=fdopen(clnt_sock, "r");
	writefp=fdopen(dup(clnt_sock), "w");
	
	fputs("FROM SERVER: Hi~ client? \n", writefp);
	fputs("I love all of the world \n", writefp);
	fputs("You are awesome! \n", writefp);
	fflush(writefp);
	
	shutdown(fileno(writefp), SHUT_WR);
	fclose(writefp);
	
	fgets(buf, sizeof(buf), readfp); fputs(buf, stdout); 
	fclose(readfp);
	return 0;
}