坚持思考,就会很酷。

大家好,这是进入大厂面试准备 第1篇文章

知识地图:LINUX系统调用

问:read 返回0是否一定表示对端关闭?

答:

  • ​​普通文件​​:当读取位置到达文件末尾(EOF)时,返回 0(跟设置 阻塞和非阻无关)

  • 管道/套接字​​:当对端关闭写端且缓冲区无数据时,返回 0 if (sk->sk_state == TCP_CLOSE) { if (!copied) return 0; // 返回EOF }

  • 为了搞清楚socket的行为,必须要研究一下对应的kernel的代码。

    1. 何时返回值为0;
    2. 对端关闭(shutdown)后,是否可以继续读取对端关闭前发送的数据呢

下面是是从0到1的学习过程。

阅读本文你讲收益如下

✅ 普通文件为设置非阻塞无效

✅ socket read函数返回0代表什么含义

  1. 何时返回值为0;
  2. 对端关闭(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的返回值?

  • 阻塞情况下 read的返回值一共有三种情况:
  1. 大于0:成功读取的字节数;
  2. 等于0:到达文件尾;
  3. -1:发生错误,通过errno确定具体错误值。

 > On success, the number of bytes read is returned (zero indicates end of file)

  • 非阻塞情况(O_NONBLOCK) read ,条件不满足可能如下

  • EAGAIN or EWOULDBLOCK

问:达文件尾是什么意思?

  • 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

  • 为了搞清楚socket的行为,必须要研究一下对应的kernel的代码。

  1. 何时返回值为0;
  2. 对端关闭(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

总结

  1. 对端关闭后,是否可以读取对端在关闭之前发送的数据 —可以
  • 继续读取缓冲区数据,直到read()返回0
  • read()返回0时,应主动关闭本端套接字以释放资源,防止长时间处于CLOSE_WAIT
​行为​ ​能否读取关闭前数据​ read()返回值​
对端close() 是(直到缓冲区空) 0(缓冲区空时)
对端shutdown(SHUT_WR) 是(直到缓冲区空) 0(缓冲区空时)
对端异常终止 否(可能部分数据丢失) -1(错误)

如果您觉得阅读本文对您有帮助, 请点一下“点赞,转发” 按钮, 您的“点赞,转发” 将是我最大的写作动力!