文件描述符(File Descriptor, FD)是Unix和类Unix操作系统中用于标识进程打开的文件、设备或其他I/O资源的一个抽象概念。它是一个非负整数,由内核在进程打开或创建一个文件时分配给该进程。
当应用程序通过系统调用如open()
、socket()
等操作打开一个现有文件、创建新文件或者创建网络套接字时,内核返回一个文件描述符。这个描述符实际上是一个指向内核维护的文件表结构的索引,该结构记录了与该文件相关的状态信息,例如当前读写位置、权限模式以及底层的数据结构指针。
程序后续可以通过这个文件描述符来执行对相关资源的各种操作,如读取数据read()
,写入数据write()
,关闭文件close()
,或是获取或改变其属性等。由于文件描述符是对实际资源的一种引用,因此它允许程序员以统一的方式处理不同类型的数据源,而不仅仅是普通文件。在多进程或多线程编程中,每个进程或线程都有独立的文件描述符表,使得它们可以各自访问不同的文件资源而不互相干扰。
poll
是一个Unix系统调用,用于检测一组文件描述符上的事件。它允许程序在多个文件描述符上等待事件,而无需阻塞主线程。poll
函数可以用于监视文件描述符上的读写事件,以及错误和挂起的连接等事件。 在使用poll
函数时,需要提供一个包含要监视的文件描述符的列表和一个指定要监视的事件类型的掩码。当调用poll
函数时,它会阻塞当前线程,直到至少有一个文件描述符上发生了指定的事件,或者超时时间到达。poll
函数在多路复用I/O中非常有用,因为它可以监视多个文件描述符,而无需阻塞主线程。这使得程序能够更高效地处理多个连接或事件,而无需创建多个线程或进程。
总结一下:
在Unix-like系统中,文件描述符(FileDescriptor,简称FD)是用于标识和访问打开的文件、设备、套接字等资源的整数。在进行网络编程时,套接字也被视为一种文件,因此套接字也有对应的文件描述符。
poll函数是用于检测多个文件描述符上的事件是否发生的系统调用。它允许程序同时监控多个套接字,以避免阻塞在单个套接字上。poll函数可以用于检测多种类型的事件,例如读就绪、写就绪、错误和挂起的连接等。
下面是一个使用poll
函数来监控客户端套接字的示例代码:
use libc::{c_int, pollfd, POLLIN, POLLOUT, POLLERR, POLLHUP};
use std::io::ErrorKind;
use std::net::SocketAddr;
use std::str::from_utf8;
use std::thread;
const TIMEOUT_MS: i64 = 1000;
struct Client {
fd: c_int,
addr: SocketAddr,
}
impl Client {
fn new(fd: c_int, addr: SocketAddr) -> Self {
Self { fd, addr }
}
}
fn handle_client(client: Client) {
let mut buffer = [0; 1024];
let pollfds = [
pollfd {
fd: client.fd,
events: POLLIN | POLLOUT | POLLERR | POLLHUP,
revents: 0,
},
];
loop {
let timeout = Some(TIMEOUT_MS);
let ret = unsafe { poll(&mut pollfds[..], timeout) };
if ret == -1 {
println!("poll failed: {}", ErrorKind::Other);
break;
} else if ret == 0 {
println!("poll timeout");
break;
}
if pollfds[0].revents & POLLIN != 0 {
let len = unsafe { recv(client.fd, &mut buffer, buffer.len(), 0) };
if len == -1 {
println!("recv failed: {}", ErrorKind::Other);
break;
} else if len == 0 {
println!("client disconnected");
break;
} else {
let message = from_utf8(&buffer).unwrap();
println!("Received message from {}: {}", client.addr, message);
}
}
if pollfds[0].revents & POLLOUT != 0 {
// Send a response to the client
let response = "Hello, client!";
let len = unsafe { send(client.fd, response.as_bytes(), response.len(), 0) };
if len == -1 {
println!("send failed: {}", ErrorKind::Other);
break;
}
}
if pollfds[0].revents & (POLLERR | POLLHUP) != 0 {
println!("poll error or hangup");
break;
}
}
// Close the client socket
unsafe {
close(client.fd);
}
}
fn main() {
// Bind a server socket to a local address
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
let listener = std::net::TcpListener::bind(addr).unwrap();
let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().unwrap() {
let stream = stream.unwrap();
let addr = stream.local_addr().unwrap();
let client = Client::new(stream.as_raw_fd(), addr);
thread::spawn(|| {
handle_client(client);
});
}
}
在上面的示例代码中,我们首先创建了一个Client
结构体,用于表示一个客户端连接。Client
结构体包含了一个文件描述符fd
和一个套接字地址addr
。
handle_client
函数用于处理来自客户端的消息。在该函数中,我们首先定义了一个pollfd
结构体数组,用于监控客户端套接字上的事件。pollfd结构体包含了一个文件描述符fd
、一个监控的事件集合events
和一个返回的事件集合revents
。我们监控的事件集合包含了读就绪POLLIN
、写就绪POLLOUT
、错误POLLERR
和挂起的连接POLLHUP
等事件。
然后,我们调用poll
函数来检测事件的发生。如果poll
函数返回错误,我们打印错误信息并退出循环。如果poll
函数超时,我们也退出循环。如果poll
函数返回有事件发生,我们检查返回的事件集合中是否包含读就绪事件POLLIN
,如果是,则调用recv
函数来读取客户端发送的消息,并打印该消息。如果返回的事件集合中包含写就绪事件POLLOUT
,则发送一条欢迎消息给客户端。如果返回的事件集合中包含错误事件POLLERR
或挂起的连接事件POLLHUP
,我们也退出循环。
最后,我们关闭客户端套接字。
在main
函数中,我们创建了一个TCP监听器,并将其绑定到本地地址。然后,我们使用incoming
方法获取一个可迭代的客户端连接流,并在每个连接上创建一个新的线程来处理该连接。