Phân Tích Luồng Xử Lý Của Hệ Thống Gọi listen Trong Nhân Linux

1. Tổng quan và môi trường phân tích

Quá trình phân tích dựa trên mã nguồn nhân Linux phiên bản 3.10. Tài liệu tham khảo chính được lấy từ kho lưu trữ mã nguồn chính thức, cho phép tra cứu chéo các hàm và cấu trúc dữ liệu mạng.

2. Tầng ứng dụng và điểm vào kernel

Tại không gian người dùng, lập trình viên gọi hàm chuẩn POSIX để chuyển socket sang trạng thái chờ kết nối:

/*
 * socket_fd: định danh socket cần lắng nghe
 * max_pending: số lượng kết nối tối đa được phép xếp hàng chờ
 */
int listen(int socket_fd, int max_pending);

Yêu cầu này sẽ đi qua lớp bao bọc hệ thống và được định tuyến tới hàm xử lý chính trong kernel. Cơ chế phân phối lệnh thông qua bảng syscall hoặc sys_socketcall tùy thuộc vào kiến trúc hệ thống.

3. Hàm sys_listen() – Cổng vào hệ thống

Trong tệp net/socket.c, kernel thực hiện các bước xác thực cơ bản và giới hạn tham số trước khi chuyển giao cho tầng giao thức:

// net/socket.c
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *target_skt;
    int ret_val, need_fput;
    int sys_limit;

    // Truy xuất cấu trúc socket từ file descriptor
    target_skt = sockfd_lookup_light(fd, &ret_val, &need_fput);
    if (target_skt) {
        // Đọc giới hạn hàng đợi toàn cục từ sysctl
        sys_limit = sock_net(target_skt->sk)->core.sysctl_somaxconn;
        
        // Nếu người dùng yêu cầu vượt quá ngưỡng hệ thống, ép buộc giảm xuống
        if ((unsigned int)backlog > sys_limit)
            backlog = sys_limit;

        // Kiểm tra bảo mật (LSM/SELinux)
        ret_val = security_socket_listen(target_skt, backlog);
        if (!ret_val)
            // Gọi hàm lắng nghe đặc thù của giao thức
            ret_val = target_skt->ops->listen(target_skt, backlog);

        fput_light(target_skt->file, need_fput);
    }
    return ret_val;
}

Con trỏ hàm target_skt->ops->listen thường trỏ đến inet_listen đối với họ giao thức IPv4/IPv6.

4. inet_listen() – Xử lý đặc thù giao thức Internet

Tại net/ipv4/af_inet.c, kernel tiến hành kiểm tra tính hợp lệ của trạng thái socket và thiết lập hàng đợi:

// net/ipv4/af_inet.c
int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char prev_state;
    int status = -EINVAL;

    lock_sock(sk);

    // Ràng buộc: chỉ áp dụng cho socket dòng chảy (TCP) và chưa kết nối
    if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
        goto cleanup;
        
    prev_state = sk->sk_state;
    // Chỉ cho phép nếu đang ở trạng thái CLOSE hoặc LISTEN
    if (!((1 << prev_state) & (TCPF_CLOSE | TCPF_LISTEN)))
        goto cleanup;

    // Nếu chưa ở trạng thái LISTEN, tiến hành khởi tạo hàng đợi
    if (prev_state != TCP_LISTEN) {
        // Bỏ qua các logic liên quan đến TCP FastOpen
        status = inet_csk_listen_start(sk, backlog);
        if (status)
            goto cleanup;
    }
    
    // Cập nhật ngưỡng chứa kết nối đã hoàn tất bắt tay
    sk->sk_max_ack_backlog = backlog;
    status = 0;

cleanup:
    release_sock(sk);
    return status;
}

5. inet_csk_listen_start() – Kích hoạt trạng thái nghe

Hàm này trong net/ipv4/inet_connection_sock.c chịu trách nhiệm cấp phát bộ nhớ cho hàng đợi chờ và đăng ký socket vào bảng băm lắng nghe:

// net/ipv4/inet_connection_sock.c
int inet_csk_listen_start(struct sock *sk, const int queue_size)
{
    struct inet_sock *inet = inet_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    
    // Cấp phát và khởi tạo cấu trúc hàng đợi chấp nhận kết nối
    int alloc_res = reqsk_queue_alloc(&icsk->icsk_accept_queue, queue_size);
    if (alloc_res != 0)
        return alloc_res;

    // Thiết lập lại các bộ đếm hàng đợi
    sk->sk_max_ack_backlog = 0;
    sk->sk_ack_backlog = 0;
    inet_csk_delack_init(sk);

    // Chuyển trạng thái TCP sang LISTEN
    sk->sk_state = TCP_LISTEN;
    
    // Xác minh cổng mạng và đưa vào bảng định tuyến lắng nghe
    if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
        inet->inet_sport = htons(inet->inet_num);
        sk_dst_reset(sk);
        // Thêm sock vào hash table toàn cục để xử lý gói SYN đến
        sk->sk_prot->hash(sk);
        return 0;
    }

    // Xử lý xung đột cổng
    sk->sk_state = TCP_CLOSE;
    __reqsk_queue_destroy(&icsk->icsk_accept_queue);
    return -EADDRINUSE;
}

6. Cơ chế cấp phát hàng đợi: reqsk_queue_alloc()

Kernel phân biệt rõ ràng giữa hai loại hàng đợi trong quá trình bắt tay ba bước TCP. Cấu trúc quản lý được định nghĩa như sau:

// include/net/request_sock.h
struct request_sock_queue {
    // Hàng đợi toàn phần (Accept Queue): chứa kết nối đã hoàn tất bắt tay
    struct request_sock *rskq_accept_head;
    struct request_sock *rskq_accept_tail;
    
    // Hàng đợi bán phần (SYN Queue): chờ phản hồi ACK từ client
    struct listen_sock  *listen_opt;
    // ... các trường đồng bộ hóa
};

Hàng đợi toàn phần hoạt động theo cơ chế FIFO, do đó chỉ cần con trỏ đầu và cuối. Ngược lại, hàng đợi bán phần cần tra cứu nhanh khi nhận gói ACK thứ ba, nên kernel sử dụng bảng băm:

// include/net/request_sock.h
struct listen_sock {
    u8   max_qlen_log;
    u32  nr_table_entries;
    // ...
    struct request_sock *syn_table[0]; // Mảng linh hoạt chứa các bucket băm
};

Quá trình tính toán kích thước và cấp phát bộ nhớ diễn ra trong net/core/request_sock.c:

// net/core/request_sock.c
int reqsk_queue_alloc(struct request_sock_queue *queue, unsigned int req_entries)
{
    size_t mem_size = sizeof(struct listen_sock);
    struct listen_sock *lopt;

    // Điều chỉnh kích thước bảng băm dựa trên tham số hệ thống
    req_entries = min_t(u32, req_entries, sysctl_max_syn_backlog);
    req_entries = max_t(u32, req_entries, 8);
    req_entries = roundup_pow_of_two(req_entries + 1);
    
    // Tính tổng bộ nhớ cần thiết cho cấu trúc và mảng băm
    mem_size += req_entries * sizeof(struct request_sock *);
    
    // Lựa chọn phương thức cấp phát tùy thuộc vào kích thước
    if (mem_size > PAGE_SIZE)
        lopt = vzalloc(mem_size);
    else
        lopt = kzalloc(mem_size, GFP_KERNEL);
        
    if (!lopt)
        return -ENOMEM;

    // Tính toán log2 của độ dài hàng đợi để tối ưu phép toán bitmask
    for (lopt->max_qlen_log = 3;
         (1 << lopt->max_qlen_log) < req_entries;
         lopt->max_qlen_log++);

    get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
    rwlock_init(&queue->syn_wait_lock);
    
    // Khởi tạo trạng thái rỗng cho hàng đợi toàn phần
    queue->rskq_accept_head = NULL;
    
    lopt->nr_table_entries = req_entries;
    write_lock_bh(&queue->syn_wait_lock);
    queue->listen_opt = lopt;
    write_unlock_bh(&queue->syn_wait_lock);

    return 0;
}

7. Tổng kết kỹ thuật

Hệ thống gọi listen thực hiện chuỗi thao tác cốt lõi sau:

  • Ánh xạ file descriptor sang cấu trúc socketsock tương ứng trong kernel.
  • Kiểm tra ràng buộc loại socket (chỉ hỗ trợ SOCK_STREAM) và trạng thái TCP hiện tại.
  • Chuyển đổi trạng thái máy trạng thái TCP sang TCP_LISTEN.
  • Khởi tạo cấu trúc hàng đợi kép: hàng đợi bán phần (SYN queue) dùng bảng băm để tra cứu nhanh, và hàng đợi toàn phần (Accept queue) dùng danh sách liên kết đơn FIFO.
  • Thiết lập ngưỡng sk->sk_max_ack_backlog dựa trên tham số backlog và giới hạn somaxconn, đồng thời reset bộ đếm hiện tại sk->sk_ack_backlog về 0.
  • Đăng ký socket vào bảng băm lắng nghe toàn cục, cho phép bộ xử lý mạng định tuyến các gói SYN đến đúng tiến trình đang chờ.

Thẻ: linux-kernel network-stack tcp-ip system-calls socket-programming

Đăng vào ngày 25 tháng 8 lúc 07:27