坚持思考,就会很酷。
大家好,这是进入大厂面试准备 第1篇文章
知识地图:LINUX系统调用
问:read 返回0是否一定表示对端关闭?
答:
-
普通文件:当读取位置到达文件末尾(EOF)时,返回 0(跟设置 阻塞和非阻无关)
-
管道/套接字:当对端关闭写端且缓冲区无数据时,返回 0
if (sk->sk_state == TCP_CLOSE) {
if (!copied)
return 0; // 返回EOF
}
-
为了搞清楚socket的行为,必须要研究一下对应的kernel的代码。
- 何时返回值为0;
- 对端关闭(shutdown)后,是否可以继续读取对端关闭前发送的数据呢?
下面是是从0到1的学习过程。
阅读本文你讲收益如下
✅ 普通文件为设置非阻塞无效
✅ socket read函数返回0代表什么含义
- 何时返回值为0;
- 对端关闭(shutdown)后,是否可以继续读取对端关闭前发送的数据呢?
什么是read函数?
- read函数是 c语言标准库( Standard C library glibc)提供的POSIX接口
- glibc 封装了对Linux系统调用。
- 系统调用访问Linux内核能力。
1
2
3
4
|
#include <unistd.h>
//read - read from a file descriptor
// System Call
ssize_t read(int fd, void *buf, size_t count);
|
普通文件读设置 O_NONBLOCK有效吗?
- 对磁盘文件,O_NONBLOCK 不起作用,read() 总是阻塞,遇 EOF 返回 0
- 非阻塞 I/O :管道、FIFO、字符设备,网络socket 才有效
- 针对普通文件无需考虑非阻塞
- 最佳实践: 对于普通文件 I/O,使用异步 I/O(AIO)或 mmap 更合适
- 对于网络/管道操作,使用非阻塞 + 事件驱动模型
非阻塞模式读普通文件无效如何证明?
- 看官方文档Note that this flag has no effect for regular files and block devices; 请注意,此标志对常规文件无效,并且 块设备
- 也就是说对普通问设置了 O_NONBLOCK照样阻塞读写。
- 案例:D 状态(Uninterruptible Sleep)的本质
D 状态表示进程正在等待不可中断的内核操作(如硬件 I/O 完成、文件系统锁等),
此时进程无法被信号唤醒或终止,必须等待操作完成
问:为什么进程有不可中断的睡眠状态 (D 状态)?
内核实现机制
尽管这种状态有时可能导致系统响应变慢,
确保数据一致性和系统稳定性
D 状态的特点
- 不响应任何信号(包括 SIGKILL)
- 无法通过 kill -9 强制终止
- 必须等待 I/O 完成或系统重启才能解除
特征 |
不可中断状态(D) |
可中断状态(S) |
响应信号 |
不响应任何信号 |
可被信号唤醒 |
典型场景 |
磁盘 I/O、硬件交互 |
等待用户输入、定时器睡眠 |
内核操作类型 |
必须原子完成的底层操作 |
可安全暂停的用户态操作 |
性能影响 |
减少上下文切换,优化吞吐量 |
频繁唤醒可能增加调度开销 |
问:请举例说明?
案例1:磁盘硬件故障导致 I/O 操作无法完成,数据库服务器无法响应请求
案例2:宕机 3 小时的 IO HANG
场景:
- 当需要将脏页面写回磁盘时,文件系统会调用
writeback
机制
- 当需要将页面交换到磁盘时,涉及的函数通常位于
mm/swap.c
- 当进程等待块设备的 I/O 操作完成时,进程也会进入不可中断的睡眠状态
问:socket read的返回值?
- 大于0:成功读取的字节数;
- 等于0:到达文件尾;
- -1:发生错误,通过errno确定具体错误值。
> On success, the number of bytes read is returned (zero indicates end of file)
问:达文件尾是什么意思?
- EOF 是 End Of File 的缩写。
- EOF 并不是一个字符,也不是文件中实际存在的内容
- 例如
1
|
#define EOF (-1) // 通常在 <stdio.h>
|
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main() {
printf("Enter text (Ctrl+D/Ctrl+Z to end):\n");
int ch; //非 `char` 类型
//char `c` 被提升为 `int` 类型的 `0xFFFFFFFF`,与 `EOF` 相等,
while ((ch = getchar()) != EOF) {
// 从键盘输入读取字符,手动触发 EOF 结束[6](@ref)
putchar(ch);
}
return 0;
}
|
大boss:socket read函数返回0,一定代表 对方关闭吗?
- 何时返回值为0;
- 对端关闭(shutdown)后,是否可以继续读取对端关闭前发送的数据呢?
问:ext4文件系统举例 说 read io流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
用户态调用 read()
↓
SYSCALL_DEFINE3(read) [fs/read_write.c]
↓
ksys_read() [fs/read_write.c]
↓
vfs_read() [fs/read_write.c]
↓
__vfs_read() [fs/read_write.c]
↓
file->f_op->read_iter() [net/socket.c]
↓
sock_read_iter() [net/socket.c]
↓
sock_recvmsg() [net/socket.c]
↓
sock->ops->recvmsg() [net/ipv4/tcp.c]
↓
tcp_recvmsg() [net/ipv4/tcp.c]
|
1. 用户空间 → 内核入口(系统调用)
1
2
3
4
5
6
7
8
9
|
// glibc/sysdeps/unix/sysv/linux/read.c
ssize_t read(int fd, void *buf, size_t count)
{
return SYSCALL_CANCEL (read, fd, buf, count);
}
syscall 指令,
通过中断或快速系统调用进入内核态,参数通过寄存器传递(`fd`, `buf`, `count`)
|
2. 虚拟文件系统(VFS)层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
https://github.com/torvalds/linux/blob/master/fs/read_write.c
// kernel/fs/read_write.c
//fd --file
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
ssize_t ret;
if (!(file->f_mode & FMODE_READ))
return -EBADF;
if (!(file->f_mode & FMODE_CAN_READ))
return -EINVAL;
ret = rw_verify_area(READ, file, pos, count);
if (ret)
return ret;
return __vfs_read(file, buf, count, pos);
}
|
3. Socket层处理
1
2
3
4
5
6
7
8
9
10
11
12
13
|
https://github.com/torvalds/linux/blob/master/net/socket.c
// kernel/net/socket.c
static ssize_t sock_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct file *file = iocb->ki_filp;
struct socket *sock = file->private_data;
struct msghdr msg = {
.msg_iter = *to,
};
return sock_recvmsg(sock, &msg, msg.msg_flags);
}
|
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
|
https://github.com/torvalds/linux/blob/master/net/socket.c#L156C1-L161C31
// static: 表示该结构体只在当前文件可见
// const: 表示结构体内容不可修改
// 结构体初始化语法 现代方式(使用 . 操作符)
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE, // 所属模块
.read_iter = sock_read_iter, // 读取操作
.write_iter = sock_write_iter, // 写入操作
.poll = sock_poll, // 轮询操作
.unlocked_ioctl = sock_ioctl, // IO控制操作
};
// file_operations 结构体中的函数指针定义
struct file_operations {
// 读操作函数指针
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
// 写操作函数指针
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
// 轮询函数指针
__poll_t (*poll) (struct file *, struct poll_table_struct *);
// IO控制函数指针
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
};
|
5.TCP协议层处理(伪代码)
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
|
//https://github.com/torvalds/linux/blob/master/net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,
int nonblock, int flags, int *addr_len)
{
struct tcp_sock *tp = tcp_sk(sk);
...
}
https://github.com/torvalds/linux/blob/master/net/ipv4/tcp.c#L3073
// 位置:net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len,
int nonblock, int flags, int *addr_len)
{
struct tcp_sock *tp = tcp_sk(sk);
int copied = 0; // 已复制的数据长度
int err = 0; // 错误码
int target; // 目标读取长度
long timeo; // 超时时间
// 1. 正常数据读取
do {
// 检查接收队列
if (skb_queue_empty(&sk->sk_receive_queue)) {
// 无数据可读,进入等待
sk_wait_data(sk, &timeo);
}
// 从接收队列复制数据
if ((copied = tcp_copy_to_user(sk, msg, len)) > 0)
return copied; // 成功读取数据
// 2. 连接关闭检查
if (sk->sk_state == TCP_CLOSE) {
if (!copied)
return 0; // 返回EOF
}
// 3. 错误检查
if (sk->sk_err) {
copied = sock_error(sk);
goto out; // 跳转到错误处理
}
// 4. 信号中断检查
if (signal_pending(current)) {
if (!copied)
copied = -ERESTARTSYS;
goto out;
}
} while (len > 0);
out:
return copied;
}
|
图片来源:The Linux Programming Interface

总结
- 对端关闭后,是否可以读取对端在关闭之前发送的数据 —可以
- 继续读取缓冲区数据,直到
read()
返回0
- 当
read()
返回0时,应主动关闭本端套接字以释放资源,防止长时间处于CLOSE_WAIT
行为 |
能否读取关闭前数据 |
read() 返回值 |
|
对端close() |
是(直到缓冲区空) |
0(缓冲区空时) |
|
对端shutdown(SHUT_WR) |
是(直到缓冲区空) |
0(缓冲区空时) |
|
对端异常终止 |
否(可能部分数据丢失) |
-1(错误) |
|
如果您觉得阅读本文对您有帮助,
请点一下“点赞,转发” 按钮,
您的“点赞,转发” 将是我最大的写作动力!