Đây là chuyên đề Operating System dành cho việc học backend và ôn tập phỏng vấn, tổng hợp kiến thức cơ bản về Operating System, process và thread, inter-process communication, lock và synchronization, memory management, virtual memory, zero-copy, I/O multiplexing, file system, Linux và Shell.
CPU scheduling không chỉ là tên các algorithm như FCFS, RR, CFS. Khi kiểm tra sự cố trong production, còn phải hiểu vì sao thread bị đưa khỏi CPU, chi phí của context switch nằm ở đâu, cũng như load average cao nhưng CPU usage thấp có ý nghĩa gì.
Đằng sau các vấn đề này là cùng một nhóm ràng buộc: số core CPU có hạn, task cần xếp hàng, scheduler chịu trách nhiệm quyết định task nào chạy trước; các metric hệ thống giúp xác định task đang tranh chấp CPU, chờ I/O, hay bị kẹt ở kernel, interrupt hoặc lớp virtualization.
Cách trực quan nhất để viết một interface lưu file là: nhận path, open một file, write dữ liệu vào đó, cuối cùng close. Khi số lượng file ít, concurrency thấp và máy không gặp sự cố, quy trình này có vẻ không khó.
Nhưng khi phỏng vấn hỏi sâu hơn, vấn đề sẽ xuất hiện. fd mà open() trả về thực sự trỏ tới đâu? Tên file có nằm trong inode không? Vì sao hai hard link có thể nhìn thấy cùng một nội dung? write() trả về thành công có nghĩa là dữ liệu đã được ghi xuống disk chưa? File log đã bị xóa, vì sao df -h vẫn hiển thị disk đầy?
System call đi từ user mode vào kernel mode chỉ là điểm bắt đầu để hiểu đường đi này. Xem sâu hơn qua một lần read(), ta sẽ gặp một số vấn đề liên quan chặt chẽ:
read()đi vào kernel bằng cách nào?- Vì sao clock interrupt có thể khiến thread đang chạy dừng lại?
- Vì sao Page Fault đôi khi là hành vi bình thường, nhưng đôi khi lại trở thành
SIGSEGV? - System call đã vào kernel thì có nhất thiết xảy ra thread context switch không?
Viết một TCP server, cách trực quan nhất là thread chính accept một connection, rồi giao cho một thread mới read, xử lý và write. Khi số connection ít, cách này hoạt động rất tốt.
Nhưng một khi số connection tăng lên hàng chục nghìn, vấn đề sẽ xuất hiện. Trong không ít bản phân phối Linux, mỗi thread mới mặc định sẽ dự trù vài MB stack, cấu hình thường gặp là 8 MB (giá trị thực tế phụ thuộc vào ulimit -s, runtime library và thuộc tính thread). Dù stack page của mười nghìn connection được cấp phát theo nhu cầu, address space đã dự trù, stack page thực sự được dùng và metadata của thread cộng lại vẫn rất lớn; nghiêm trọng hơn là hàng nghìn, hàng chục nghìn thread cùng tranh nhau vài CPU core, chỉ riêng context switch giữa các thread đã ngốn hơn nửa CPU, thời gian thực sự làm việc còn lại chẳng bao nhiêu. Chưa kể phần lớn connection thực ra đang idle — mỗi connection chiếm một thread nhưng chỉ ngồi chờ dữ liệu.
Ý tưởng trực giác nhất khi hai process muốn trao đổi một đoạn dữ liệu là: process A ghi dữ liệu vào memory của mình, rồi process B đọc trực tiếp là xong.
Tuy nhiên, cách này không thực hiện được trong operating system. Mỗi process có một virtual address space độc lập; địa chỉ 0x7f... trong process A và địa chỉ 0x7f... trong process B không trỏ tới cùng một vùng memory. Các process ở user space không thể tùy ý truy cập memory của nhau; nếu không, isolation về quyền sẽ mất ý nghĩa.
Giới thiệu ngắn gọn một số khái niệm và lệnh Linux thường gặp mà lập trình viên Java cần biết.
Làm quen với Linux
Giới thiệu Linux
Có thể khái quát Linux là gì qua ba điểm sau:
- Hệ điều hành kiểu Unix: Linux là một operating system tự do, mã nguồn mở và tương tự Unix.
- Linux về bản chất là Linux kernel: Nói chính xác, từ Linux chỉ biểu thị Linux kernel; bản thân Linux kernel không thể trở thành một operating system hoạt động bình thường. Vì vậy mới có nhiều Linux distribution.
- Cha đẻ của Linux (Linus Benedict Torvalds): Một nhân vật huyền thoại trong lĩnh vực lập trình, một cao thủ thực thụ và hình mẫu đáng ngưỡng mộ. Ông là tác giả đầu tiên của Linux kernel, sau đó khởi xướng open-source project này và giữ vai trò kiến trúc sư chính của Linux kernel. Ông cũng khởi xướng Git, một open-source project, và là developer chủ chốt.
Hai thread đồng thời cộng 1 vào cùng một counter, nhìn thì chỉ là một việc rất nhỏ nhưng kết quả cuối cùng lại có thể bị thiếu một lần tăng.
Nguyên nhân thực ra khá đơn giản. Trong source code, count++ chỉ là một dòng, nhưng khi máy thực thi thường phải trải qua các bước đọc, tính toán và ghi trở lại. Thread A vừa đọc được giá trị cũ nhưng chưa ghi trở lại; thread B cũng đọc được đúng giá trị cũ đó. Mỗi bên tự tính ra giá trị mới, nhưng cuối cùng lại ghi cùng một kết quả.
Lập trình Shell rất hữu ích trong công việc phát triển hằng ngày. Hiện nay, các ngôn ngữ tự động hóa vận hành phổ biến nhất trên hệ thống Linux là Shell và Python.
Bài viết này sẽ tổng hợp ngắn gọn kiến thức cơ bản về lập trình Shell, giúp bạn làm quen với lập trình Shell!
Ghi chú về phiên bản
Trong phỏng vấn thường có một kiểu hỏi quen thuộc: trước tiên hỏi bạn “Vì sao Kafka nhanh?”, “Vì sao RocketMQ chịu được lượng tồn đọng lớn?”. Sau khi bạn trả lời được về sequential write, Page Cache và zero-copy, người phỏng vấn sẽ đào sâu: “Cụ thể zero-copy đã loại bỏ những lần copy nào?”, “mmap và sendfile khác nhau thế nào?”, “splice dùng để làm gì?”.
