文件描述符
文件描述符(File Descriptor, FD)是操作系统中用于访问文件的一个抽象概念。它是一个非负整数,通常由操作系统分配,用来标识被打开的文件或输入输出资源(如管道、网络连接等)。文件描述符在操作系统和应用程序之间充当桥梁,允许程序通过文件描述符来读取、写入文件或进行其他I/O操作。
文件描述符的类型
文件描述符通常分为三类标准描述符:
- 标准输入(Standard Input,FD 0):
- 默认情况下与键盘关联,通常用于从用户那里接收输入数据。
- 标准输出(Standard Output,FD 1):
- 默认情况下与终端窗口关联,通常用于向用户显示输出数据。
- 标准错误(Standard Error,FD 2):
- 默认情况下也与终端窗口关联,但通常用于显示错误消息或诊断信息。
文件描述符的使用
在UNIX和类UNIX操作系统中,文件描述符用于各种I/O操作,包括:
- 打开文件:
open()
系统调用返回一个文件描述符,表示已打开的文件。 - 读取文件:
read()
系统调用使用文件描述符从文件中读取数据。 - 写入文件:
write()
系统调用使用文件描述符将数据写入文件。 - 关闭文件:
close()
系统调用使用文件描述符关闭文件,以释放系统资源。
文件描述符不仅限于文件,还可以用于网络套接字(socket)、管道(pipe)、设备文件等各种输入输出资源。通过文件描述符,程序可以对这些资源进行抽象的统一操作。
socket 和文件描述符的关系
socket 和文件描述符之间有着密切的关系,特别是在 UNIX 和类 UNIX 操作系统中。简而言之,socket 是一种特殊类型的文件描述符,它用于网络通信。
Socket 与文件描述符的关系
- Socket 是文件描述符的一种:
- 在操作系统中,socket 被抽象为文件,这意味着每个 socket 都可以通过文件描述符进行标识和操作。文件描述符不仅用于文件,还可以用于其他 I/O 资源,如 socket、管道、设备文件等。
- Socket 的创建与文件描述符:
- 当你使用
socket()
系统调用创建一个 socket 时,操作系统会返回一个文件描述符,这个文件描述符代表了创建的 socket。例如:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
这里,sockfd
就是一个文件描述符,后续的所有 socket 操作(如连接、发送、接收等)都将通过该文件描述符来进行。
- Socket 的操作类似于文件操作:
- 和普通文件一样,socket 的读写操作也是通过
read()
、write()
甚至是send()
、recv()
等系统调用来完成的。你可以使用这些调用函数来向 socket 发送或接收数据。例如:
char buffer[1024];
int n = read(sockfd, buffer, sizeof(buffer));
在这个例子中,read()
函数通过 sockfd
文件描述符从 socket 中读取数据。
- Socket 的关闭:
- 当 socket 不再需要使用时,可以使用
close()
系统调用关闭它,就像关闭文件一样。关闭操作将释放与该文件描述符相关的所有资源:
close(sockfd);
- 重定向与 socket:
- 在某些高级应用中,可以通过重定向文件描述符来实现 socket 与其他文件描述符的交换。例如,将标准输入/输出重定向到一个 socket,从而通过网络连接来读写数据。
pipe 函数
用于创建管道,实现进程之间的通信。
#include <unistd.h>
//成功返回0,失败返回-1并设置errno
int pipe(int fd[2]);
fd[1]
只能用于数据写入。fd[0]
只能用于数据读出。
socket 的基础 API 中有一个 socketpair
函数:
#include <sys/types.h>
#include <sys/socket.h>
//成功返回0,失败返回-1设置errno
int socketpair(int domain, int type, int protocol, int fd[2]);
domain
只能使用UNIX本地协议族AF_UNIX
,所以socketpair
只能在本地使用,不过创建的这对文件描述符都是可读可写的。
dup函数和dup2函数
可以将标准输入重定向到一个文件,或者标准输出重定向到一个网络连接。
#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
以下程序使用dup函数实现了一个基本的CGI服务器:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <libgen.h>
int main(int argc, char *argv[]) {
if (argc <= 2) {
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
// 创建套接字
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
// 绑定套接字
int ret = bind(sock, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
// 监听套接字,最大等待连接队列的长度为5
ret = listen(sock, 5);
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
// 接受客户端连接
int connfd = accept(sock, (struct sockaddr *)&client, &client_addrlength);
if (connfd < 0) {
printf("errno is: %d\n", errno);
} else {
// 先关闭标准输出文件描述符STDOUT_FILENO,其值为1
close(STDOUT_FILENO);
// 复制socket文件描述符connfd,由于dup函数总是返回系统中最小的可用文件描述符
// 因此dup参数实际返回的是1,即之前关闭的标准输出文件描述符的值
// 这样服务器输出到标准输出的内容会直接发送到与客户连接对应的socket上
dup(connfd);
printf("abcd\n");
close(connfd);
}
close(sock);
return 0;
}
代码首先关闭标准输出文件描述符STDOUT_FILENO
,其值为1(由宏定义);
dup(connfd);
复制客户端连接的文件描述符,并将其重定向为标准输出文件描述符1
(即STDOUT_FILENO
);
之后的printf("abcd\n");
语句输出的内容将通过套接字发送给客户端(而非终端)。
char* a[3];
与 char a[3];
char* a[3];
与 char a[3];
之间的主要区别在于:
1. 类型和结构的不同:
char a[3];
:
- 这是一个包含3个字符元素的字符数组。
- 它占用3个字节的连续内存空间,每个元素都是一个
char
类型的字符。 - 适用于存储一组字符,通常用于存储小的字符串或字符数据。
char* a[3];
:
- 这是一个包含3个元素的指针数组,每个元素都是指向
char
类型数据的指针。 - 这意味着你可以在这个数组中存储3个字符指针,每个指针可以指向一个字符串或字符数组。
- 适用于存储字符串的指针或一组字符数组的指针。
2. 内存布局的不同:
char a[3];
:
- 内存中会有3个连续的字节,每个字节存储一个字符。
- 例如,
a[0]
、a[1]
和a[2]
都存储在相邻的内存地址中。
char* a[3];
:
- 内存中会有3个连续的指针,每个指针指向一个
char
类型的数据。 - 这些指针本身占用空间(在32位系统中每个指针占用4个字节,在64位系统中每个指针占用8个字节),它们指向的内容可以是任意内存位置的字符或字符串。
- 例如,
a[0]
、a[1]
和a[2]
存储的是指针,而不是字符本身。
3. 使用场景的不同:
char a[3];
:
- 适用于存储一小段字符数据,例如单个短字符串。
- 例如,你可以用它来存储字符串
"Hi"
,并以'\0'
结尾。
char* a[3];
:
- 适用于存储多个字符串的指针,通常用于二维字符数组或字符串数组的场景。
- 例如:
char* a[3];
a[0] = "Hello";
a[1] = "World";
a[2] = "!";
- 这里
a[0]
、a[1]
和a[2]
分别指向三个不同的字符串常量。
总结
char a[3];
是一个存储字符的数组。
char* a[3];
是一个存储字符指针的数组,通常用于存储字符串的指针。
readv函数和writev函数
readv
函数将数据从文件描述符读到分散的内存块中,即分散读;writev
函数将多块分散的内存数据一并写入文件描述符中,即集中写:
#include <sys/uio.h>
//成功返回读写的字节数,失败返回-1并设置errno
ssize_t readv(int fd, const struct iovec* vector, int count); //分散读
ssize_t writev(int fd, const struct iovec* vector, int count); //集中写
结构体 iovec
:
struct iovec{
void *iov_base; //内存块起始地址
size_t iov_len; //内存块长度
};
sendfile函数
sendfile
函数在两个文件描述符之间直接传递数据,完全在内核中操作,避免内核缓冲区和用户缓冲区之间的数据拷贝,效率高。这被称为零拷贝。
#include <sys/sendfile.h>
//成功返回传输的字节数,失败返回-1并设置errno
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
参数:
in_fd
:待读出内容的文件描述符out_fd
:待写入内容的文件描述符offset
:指定从读入文件流的哪个位置开始读,如果为空,则使用默认起始位置count
:指定in_fd
和out_fd
之间传输的字节数。
mmap函数和munmap函数
mmap
用于申请一段内存空间,这段内存可以用于进程间通信的共享内存,也可以直接将文件映射到其中,munmap
用于释放这段空间。
#include <sys/mman.h>
//成功返回指向目标区域的指针,失败返回MAP_FAILED((void*) -1),并设置errno
void* mmap(void *start, size_t length, int port, int flags, int fd, off_t offset);
//成功返回0,失败返回-1并设置errno
int munmap(void *start, size_t length);
参数:
start
:允许用户使用某一个特定的地址作为这段内存的起始地址,如果是设置为NULL
,则系统自动分配。length
:指定这段内存的长度port
:设置内存段的访问权限。
-
PROT_READ
,内存段可读。PROT_WRITE
,内存段可写。PROT_EXEC
,内存段可执行。PROT_NONE
,内存段不能被访问。
flags
:控制内存段内容被修改后 程序的行为。
fd
:是被映射文件的文件描述符,一般通过open
系统调用获取offset
:设置从文件的何处开始映射。
splice函数
用于两个文件描述符之间移动数据,也是零拷贝操作。
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);
参数:
fd_in
:是输入数据的文件描述符,用于数据的读出。fd_out
和off_out
含义类似,用于输出数据流(数据写入)。len
:指定移动数据的长度。flags
:控制数据如何移动。
tee函数
tee
函数在两个管道文件描述符之间复制数据,也是零拷贝操作,不消耗数据,而splice
从管道中读取数据,也就是消耗数据。
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
参数:
fd_in
和fd_out
必须都是管道文件描述符
fcntl函数
fcntl
函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制,另一个常见的控制文件描述符属性和行为的系统调用是ioctl
,且ioctl
函数比fcntl
函数能执行更多的控制,但控制文件描述符的常用属性和操作,fcntl
函数是由 POSIX 规范指定的首选方法:
#include <fcntl.h>
//失败返回-1并设置errno
int fcntl(int fd, int cmd, ...);
参数:
fd
:被操作的文件描述符cmd
:指定执行何种类型的操作- 由于操作类型的不同,可能需要第三个可选参数
arg
fcntl
支持的常用操作:
在网络编程中,fcntl
函数常用于把文件描述符设置为非阻塞的:
int setnonblocking(int fd) {
// 获取文件描述符状态标志
int old_option = fcntl(fd, F_GETFL);
// 设置非阻塞标志
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
// 返回fd旧的状态标志,以便日后恢复该状态标志
return old_option;
}