Bỏ qua

Hệ Điều Hành

Hệ điều hành là lớp phần mềm giữa phần cứng và ứng dụng, quản lý tài nguyên, cung cấp các lớp trừu tượng và thực thi sự cô lập. File này bao gồm chức năng của hệ điều hành, tiến trình, luồng, lập lịch CPU, quản lý bộ nhớ, hệ thống file và lời gọi hệ thống

  • Một máy tính không có hệ điều hành giống như một căn bếp không có đầu bếp: nguyên liệu (phần cứng) có đó, nhưng không có gì phối hợp ai dùng bếp, để bát đĩa ở đâu, hay ngăn hai người lấy cùng một con dao. Hệ điều hành (OS) là người điều phối đó.

  • Đối với người làm ML, các khái niệm về hệ điều hành giải thích: tại sao nvidia-smi hiển thị mức sử dụng bộ nhớ GPU trên mỗi tiến trình, tại sao huấn luyện crash với lỗi "out of memory," tại sao fork() nhân bản tiến trình Python của bạn, và tại sao container Docker cung cấp môi trường cô lập.

Hệ Điều Hành Làm Gì

  • Hệ điều hành có ba trách nhiệm cốt lõi:

    • Trừu tượng hóa (Abstraction): ẩn độ phức tạp của phần cứng đằng sau các giao diện sạch sẽ. Các chương trình đọc và ghi "file" mà không cần biết bộ nhớ bên dưới là SSD, HDD hay ổ đĩa mạng. Chúng cấp phát "bộ nhớ" mà không quản lý các chip RAM vật lý. Chúng chạy trên "CPU" mà không lo lắng về ngắt và sự nhất quán cache.

    • Quản lý tài nguyên: nhiều chương trình chia sẻ CPU, bộ nhớ, đĩa và mạng. Hệ điều hành quyết định ai nhận được gì, khi nào và trong bao lâu. Một chiến lược phân bổ công bằng, hiệu quả giữ cho hệ thống phản hồi nhanh.

    • Cô lập và bảo vệ: các chương trình không được can thiệp lẫn nhau. Một lỗi trong trình duyệt web của bạn không nên làm sập kernel. Một chương trình độc hại không nên đọc mật khẩu của chương trình khác. Hệ điều hành thực thi các ranh giới bằng cách sử dụng hỗ trợ phần cứng (các mức đặc quyền, bộ nhớ ảo).

Tiến Trình (Processes)

  • Một tiến trình là một chương trình đang chạy. Nó là đơn vị công việc cơ bản của hệ điều hành. Mỗi tiến trình có:

    • (các chỉ thị chương trình, chỉ đọc).
    • Dữ liệu (biến toàn cục, cấp phát heap).
    • Ngăn xếp (Stack) (khung gọi hàm, biến địa phương).
    • Trạng thái (giá trị thanh ghi, bộ đếm chương trình, file đang mở, v.v.).
  • Khối điều khiển tiến trình (PCB) là cấu trúc dữ liệu của hệ điều hành để theo dõi một tiến trình. Nó lưu trữ ID tiến trình (PID), trạng thái, bộ đếm chương trình, nội dung thanh ghi, bản đồ bộ nhớ, mô tả file đang mở và độ ưu tiên lập lịch. Khi hệ điều hành chuyển từ tiến trình này sang tiến trình khác, nó lưu trạng thái của tiến trình hiện tại vào PCB của nó và nạp trạng thái của tiến trình tiếp theo. Đây gọi là chuyển đổi ngữ cảnh (context switch).

  • Chuyển đổi ngữ cảnh rất đắt: lưu và khôi phục thanh ghi, xả cache và vô hiệu hóa các mục TLB tốn micro giây. Trên một hệ thống chạy hàng ngàn tiến trình, chi phí này có thể đáng kể. Đây là lý do các kiến trúc máy chủ process-per-request (như Apache cũ) đã được thay thế bằng kiến trúc dựa trên luồng hoặc hướng sự kiện.

  • Tạo tiến trình trong Unix sử dụng fork()exec():

    • fork() tạo một bản sao của tiến trình hiện tại. Tiến trình con nhận được một bản sao của bộ nhớ, mô tả file và trạng thái của cha. Cả hai tiến trình tiếp tục thực thi từ cùng một điểm, nhưng fork() trả về 0 trong tiến trình con và PID của con trong tiến trình cha.

    • exec() thay thế mã của tiến trình hiện tại bằng một chương trình mới. Sau fork(), tiến trình con thường gọi exec() để chạy một chương trình khác.

    • Mô hình fork-then-exec này rất thanh lịch: tạo một tiến trình mới (fork) và nạp một chương trình mới (exec) là các phép toán riêng biệt có thể được tùy chỉnh độc lập. Giữa fork và exec, tiến trình con có thể chuyển hướng I/O, thay đổi biến môi trường hoặc loại bỏ đặc quyền.

Chuyển đổi trạng thái tiến trình: mới → sẵn sàng → đang chạy → bị chặn/kết thúc, với hành động ưu tiên và chờ I/O

  • Trạng thái tiến trình: một tiến trình ở một trong nhiều trạng thái:
    • Đang chạy (Running): hiện đang thực thi trên một lõi CPU.
    • Sẵn sàng (Ready): đang chờ một lõi CPU (có thể chạy nhưng chưa được lập lịch).
    • Bị chặn (Blocked): không thể tiếp tục cho đến khi một sự kiện nào đó xảy ra (I/O hoàn tất, giành được khóa, hết thời gian chờ).
    • Kết thúc (Terminated): đã hoàn thành thực thi, đang chờ cha thu thập trạng thái thoát.

Luồng (Threads)

  • Một luồng là một đơn vị thực thi nhẹ trong một tiến trình. Tất cả các luồng trong một tiến trình chia sẻ cùng mã, dữ liệu và heap, nhưng mỗi luồng có ngăn xếp và trạng thái thanh ghi riêng.

  • Lợi thế so với nhiều tiến trình: các luồng chia sẻ bộ nhớ, vì vậy giao tiếp giữa chúng nhanh (chỉ cần đọc/ghi biến dùng chung). Các tiến trình yêu cầu giao tiếp liên tiến trình (IPC - pipes, socket, ánh xạ bộ nhớ dùng chung), chậm hơn và phức tạp hơn.

  • Bất lợi: bộ nhớ dùng chung rất nguy hiểm. Hai luồng ghi vào cùng một biến đồng thời gây ra điều kiện đua (race condition) (kết quả phụ thuộc vào luồng nào chạy trước). Điều này dẫn chúng ta đến đồng bộ hóa, được đề cập trong file 4.

  • Luồng kernel được quản lý bởi bộ lập lịch của hệ điều hành. Mỗi luồng được lập lịch độc lập lên các lõi CPU. Tạo và chuyển đổi luồng kernel liên quan đến lời gọi hệ thống, với chi phí tương tự (nhưng ít hơn) so với chuyển đổi ngữ cảnh tiến trình.

  • Luồng người dùng (green threads) được quản lý bởi một thư viện runtime trong không gian người dùng, vô hình đối với hệ điều hành. Chúng rẻ hơn để tạo và chuyển đổi (không cần lời gọi hệ thống), nhưng một thao tác chặn của một luồng người dùng sẽ chặn tất cả các luồng trong tiến trình (vì hệ điều hành chỉ thấy một luồng kernel).

  • Các hệ thống hiện đại sử dụng mô hình lai: nhiều luồng người dùng ánh xạ lên một số nhỏ hơn các luồng kernel (luồng M:N). Goroutine của Go và các tiến trình của Erlang là các luồng cấp người dùng được lập lịch bởi runtime ngôn ngữ lên các luồng hệ điều hành.

  • Thread pool tạo trước một số lượng cố định các luồng chờ tác vụ. Khi một tác vụ đến, nó được gán cho một luồng rảnh. Điều này tránh chi phí tạo và hủy luồng cho mỗi tác vụ. Máy chủ web, cơ sở dữ liệu và máy chủ suy luận ML đều sử dụng thread pool.

Lập Lịch CPU

  • Bộ lập lịch quyết định tiến trình/luồng nào chạy trên lõi CPU nào tại mỗi thời điểm. Các mục tiêu là: tối đa hóa sử dụng CPU, tối thiểu hóa thời gian phản hồi (cho tác vụ tương tác), tối đa hóa thông lượng (cho tác vụ batch) và đảm bảo sự công bằng.

  • Đến trước phục vụ trước (FCFS): các tiến trình chạy theo thứ tự đến. Đơn giản nhưng mắc hiệu ứng đoàn xe (convoy effect): một tiến trình chạy lâu chặn tất cả các tiến trình ngắn hơn phía sau.

  • Công việc ngắn nhất trước (SJF): chạy tiến trình ngắn nhất trước. Được chứng minh là tối thiểu hóa thời gian chờ trung bình, nhưng đòi hỏi biết trước độ dài công việc (không thể trong trường hợp tổng quát). Phiên bản có ưu tiên, Thời gian còn lại ngắn nhất trước (SRTF), ngắt một công việc đang chạy nếu một công việc ngắn hơn đến.

  • Round Robin (RR): mỗi tiến trình nhận một lượng tử thời gian (time quantum) cố định (ví dụ, 10 ms), sau đó bị ưu tiên và chuyển về cuối hàng đợi. Công bằng và phản hồi nhanh, nhưng lượng tử thời gian quan trọng: quá nhỏ gây chuyển đổi ngữ cảnh quá mức, quá lớn thì suy biến thành FCFS.

  • Lập lịch ưu tiên: mỗi tiến trình có một độ ưu tiên. Các tiến trình có độ ưu tiên cao hơn chạy trước. Nguy hiểm là sự chết đói (starvation): các tiến trình có độ ưu tiên thấp có thể không bao giờ được chạy nếu các tiến trình ưu tiên cao liên tục đến. Aging giải quyết điều này: độ ưu tiên của một tiến trình tăng lên khi nó chờ càng lâu.

  • Hàng đợi phản hồi đa mức (MLFQ): nhiều hàng đợi với các độ ưu tiên và lượng tử thời gian khác nhau. Các tiến trình mới bắt đầu trong hàng đợi ưu tiên cao nhất (lượng tử ngắn). Nếu một tiến trình sử dụng hết lượng tử của nó (CPU-bound), nó bị giáng xuống hàng đợi ưu tiên thấp hơn (lượng tử dài hơn). Các tiến trình tương tác tự nhiên ở lại trong các hàng đợi ưu tiên cao (chúng bị chặn vì I/O trước khi dùng hết lượng tử). Điều này thích ứng với khối lượng công việc mà không cần biết trước loại công việc.

  • Bộ lập lịch hoàn toàn công bằng (CFS): bộ lập lịch của Linux. Nó duy trì một cây đỏ-đen (cây tìm kiếm nhị phân cân bằng) các tiến trình được sắp xếp theo "thời gian chạy ảo" — lượng thời gian CPU mà chúng đã tiêu thụ. Tiến trình có thời gian chạy ảo nhỏ nhất chạy tiếp theo. Điều này đảm bảo rằng theo thời gian, mọi tiến trình đều nhận được phần công bằng của nó. CFS chạy trong \(O(\log n)\) mỗi quyết định lập lịch.

Quản Lý Bộ Nhớ

  • Hệ điều hành quản lý RAM vật lý, cấp phát nó cho các tiến trình và thu hồi khi không còn cần thiết.

  • Phân trang (Paging) (từ file 2) chia bộ nhớ ảo thành các trang có kích thước cố định và bộ nhớ vật lý thành các khung. Bảng trang ánh xạ trang đến khung. Phân trang loại bỏ phân mảnh ngoài (không gian lãng phí giữa các cấp phát) vì tất cả các trang đều có cùng kích thước.

  • Phân trang theo yêu cầu (Demand paging) nạp các trang vào RAM chỉ khi chúng được truy cập lần đầu (không phải khi tiến trình bắt đầu). Điều này tiết kiệm bộ nhớ: một chương trình với 1 GB mã có thể chỉ sử dụng 50 MB trong một lần chạy điển hình. Phần còn lại không bao giờ được nạp.

  • Khi RAM đầy và cần một trang mới, hệ điều hành phải đẩy ra (evict) một trang hiện có. Các thuật toán thay thế trang (LRU, FIFO, clock, từ file 2) quyết định trang nào sẽ bị đẩy. Thay thế tốt tối thiểu hóa lỗi trang; thay thế tồi gây thrashing.

  • Phân đoạn (Segmentation) chia bộ nhớ thành các đoạn có kích thước thay đổi (mã, dữ liệu, ngăn xếp, heap), mỗi đoạn có địa chỉ cơ sở và độ dài riêng. Phân đoạn cung cấp tổ chức logic, trong khi phân trang cung cấp quản lý vật lý. Các hệ thống hiện đại sử dụng phân đoạn tối thiểu (chủ yếu cho bảo vệ) và dựa vào phân trang để quản lý bộ nhớ.

  • Heap là nơi bộ nhớ được cấp phát động (malloc/free trong C, new trong Java, ngầm định trong Python). Hệ điều hành cung cấp các khối bộ nhớ lớn cho tiến trình, và một bộ cấp phát bộ nhớ (ví dụ, glibc malloc, jemalloc, tcmalloc) chia nhỏ các khối này thành các cấp phát nhỏ hơn. Thiết kế bộ cấp phát ảnh hưởng đến hiệu suất: phân mảnh lãng phí không gian, và tranh chấp giữa các luồng lãng phí thời gian.

Hệ Thống File

  • Một hệ thống file tổ chức dữ liệu trên bộ nhớ liên tục (SSD, HDD) dưới dạng một hệ thống phân cấp các file và thư mục có tên.

  • Một inode (index node) lưu trữ siêu dữ liệu của một file: kích thước, quyền sở hữu, quyền truy cập, dấu thời gian và các con trỏ đến các khối dữ liệu trên đĩa. Tên file được lưu trong thư mục, nơi ánh xạ tên đến số inode. Sự tách biệt này có nghĩa là một file có thể có nhiều tên (hard link) trỏ đến cùng một inode.

  • FAT (File Allocation Table): một hệ thống file đơn giản được sử dụng trên ổ USB và thẻ SD. Một bảng ánh xạ mỗi cluster (khối) đến cluster tiếp theo trong file, tạo thành một danh sách liên kết. Đơn giản nhưng không hỗ trợ quyền truy cập, journaling hoặc file lớn tốt.

  • ext4: hệ thống file mặc định của Linux. Sử dụng inode với các con trỏ khối trực tiếp, gián tiếp, gián tiếp kép và gián tiếp ba để xử lý file ở mọi kích thước. Hỗ trợ extents (các dải khối liền kề) để hiệu quả với file lớn. Kích thước file tối đa: 16 TB, phân vùng tối đa: 1 EB.

  • Journaling bảo vệ chống hỏng dữ liệu do sập hệ thống. Trước khi sửa đổi cấu trúc hệ thống file, các thay đổi được ghi vào một journal (nhật ký). Nếu hệ thống sập giữa chừng, journal được phát lại khi khởi động lại để hoàn tất hoặc hủy thao tác. Không có journaling, một sự cố trong khi ghi có thể để lại hệ thống file ở trạng thái không nhất quán (khối dữ liệu của file đã được cập nhật nhưng inode của nó thì không, hoặc ngược lại).

  • Hệ thống file dựa trên B-tree (Btrfs, ZFS) sử dụng B-tree (cây tìm kiếm cân bằng) để tổ chức dữ liệu và siêu dữ liệu, cho phép tìm kiếm hiệu quả, ảnh chụp nhanh copy-on-write và checksum tích hợp để đảm bảo tính toàn vẹn dữ liệu. Đây là cùng loại B-tree được sử dụng trong các chỉ mục cơ sở dữ liệu.

Lời Gọi Hệ Thống và Chế Độ Kernel

  • Một lời gọi hệ thống (system call) là giao diện giữa chương trình người dùng và kernel hệ điều hành. Khi một chương trình cần làm điều gì đó có đặc quyền (đọc file, cấp phát bộ nhớ, tạo tiến trình, gửi gói tin mạng), nó thực hiện một lời gọi hệ thống.

  • CPU hoạt động trong hai chế độ:

    • Chế độ người dùng (User mode): bị hạn chế. Các chương trình có thể thực thi mã riêng và truy cập bộ nhớ riêng, nhưng không thể trực tiếp truy cập phần cứng, bộ nhớ của tiến trình khác hoặc cấu trúc dữ liệu hệ điều hành.
    • Chế độ kernel (Kernel mode): không bị hạn chế. Kernel hệ điều hành có thể truy cập tất cả phần cứng và bộ nhớ. Lời gọi hệ thống là cổng được kiểm soát từ chế độ người dùng sang chế độ kernel.
  • Khi một chương trình gọi read(), các bước sau xảy ra:

    1. Chương trình đặt các đối số vào thanh ghi và kích hoạt một trap (một ngắt phần mềm).
    2. CPU chuyển sang chế độ kernel và nhảy đến bộ xử lý lời gọi hệ thống.
    3. Kernel xác thực các đối số, thực hiện thao tác I/O và sao chép dữ liệu vào bộ đệm của người dùng.
    4. Kernel chuyển trở lại chế độ người dùng và trả về kết quả.
  • Các lời gọi hệ thống phổ biến: open, read, write, close (file), fork, exec, wait, exit (tiến trình), mmap, brk (bộ nhớ), socket, bind, listen, accept (mạng).

  • Ngắt (Interrupts) là các tín hiệu phần cứng buộc CPU tạm thời dừng việc đang làm và chạy một bộ xử lý ngắt (trong kernel). Một lần nhấn phím, một gói tin mạng đến, hoặc một nhịp timer đều tạo ra ngắt. Ngắt timer đặc biệt quan trọng: nó cho phép hệ điều hành ưu tiên một tiến trình đang chạy và chuyển sang tiến trình khác (đa nhiệm có ưu tiên - preemptive multitasking).

Kiến Thức Cơ Bản Về Mạng

  • Ngăn xếp mạng là một hệ thống con của hệ điều hành cho phép giao tiếp giữa các máy. Hiểu về nó giải thích cách huấn luyện phân tán đồng bộ hóa gradient, cách phục vụ mô hình xử lý yêu cầu và tại sao độ trễ quan trọng.

Ngăn xếp TCP/IP: các lớp ứng dụng, vận chuyển, mạng và liên kết, mỗi lớp thêm header

  • Mô hình TCP/IP tổ chức mạng thành các lớp, mỗi lớp cung cấp một sự trừu tượng cho lớp bên trên:

    • Lớp liên kết (Link layer): xử lý giao tiếp trên một liên kết vật lý đơn lẻ (Ethernet, Wi-Fi). Liên quan đến địa chỉ MAC và khung (frame).
    • Lớp mạng (IP): định tuyến các gói tin qua nhiều mạng từ nguồn đến đích. Mỗi máy có một địa chỉ IP (ví dụ, 192.168.1.1 cho IPv4, hoặc địa chỉ IPv6 128-bit). Router chuyển tiếp gói tin từng chặng dựa trên IP đích.
    • Lớp vận chuyển (TCP/UDP): cung cấp giao tiếp đầu cuối giữa các ứng dụng.
    • Lớp ứng dụng (Application layer): các giao thức như HTTP, DNS, gRPC mà các ứng dụng sử dụng trực tiếp.
  • TCP (Giao thức Điều khiển Truyền vận) cung vấp phân phối đáng tin cậy, có thứ tự. Nó thiết lập kết nối (bắt tay ba bước: SYN, SYN-ACK, ACK), đảm bảo tất cả dữ liệu đến đúng thứ tự (sử dụng số thứ tự và xác nhận), truyền lại các gói bị mất và kiểm soát tốc độ gửi để tránh làm quá tải mạng (kiểm soát tắc nghẽn). Cái giá là độ trễ: bắt tay thêm một vòng trip, và việc truyền lại thêm độ trễ.

  • UDP (Giao thức Gói dữ liệu Người dùng) cung cấp phân phối không đáng tin cậy, không có thứ tự. Không bắt tay, không truyền lại, không đảm bảo thứ tự. Độ trễ thấp hơn nhiều so với TCP. Được sử dụng khi tốc độ quan trọng hơn độ tin cậy: truyền phát video, chơi game trực tuyến, tra cứu DNS. Trong ML, một số giao thức đồng bộ hóa gradient sử dụng RDMA dựa trên UDP để có độ trễ thấp hơn.

  • Socket là API của hệ điều hành cho giao tiếp mạng. Một socket là một điểm cuối được xác định bởi (địa chỉ IP, số cổng). Máy chủ tạo một socket, gắn nó vào một cổng (ví dụ, 80 cho HTTP), lắng nghe kết nối và chấp nhận chúng. Máy khách tạo một socket và kết nối đến địa chỉ:cổng của máy chủ. Dữ liệu sau đó được đọc và ghi qua socket giống như một file.

  • DNS (Hệ thống Tên Miền) dịch tên có thể đọc được bởi con người (google.com) thành địa chỉ IP (142.250.80.46). Nó là một cơ sở dữ liệu phân tán, có phân cấp: máy của bạn hỏi một bộ phân giải địa phương, bộ phân giải hỏi máy chủ gốc (root server), máy chủ gốc ủy quyền cho các máy chủ có thẩm quyền cho mỗi tên miền.

  • HTTP (Giao thức Truyền tải Siêu văn bản) là giao thức yêu cầu-phản hồi của web. Máy khách gửi một yêu cầu (phương thức + URL + header + phần thân tùy chọn), và máy chủ gửi một phản hồi (mã trạng thái + header + phần thân). Phục vụ mô hình ML (ví dụ, TensorFlow Serving, Triton) để lộ các mô hình dưới dạng endpoint HTTP hoặc gRPC.

  • Độ trễ vs băng thông: độ trễ là thời gian để một gói tin đi từ nguồn đến đích (được xác định bởi khoảng cách vật lý và số chặng mạng). Băng thông là tốc độ dữ liệu (bao nhiêu byte mỗi giây). Một kết nối băng thông cao, độ trễ cao (internet vệ tinh) có thể truyền nhiều dữ liệu nhưng mỗi byte mất nhiều thời gian để đến nơi. Đối với huấn luyện phân tán, độ trễ quan trọng cho các rào cản đồng bộ hóa (tất cả GPU phải chờ GPU chậm nhất), trong khi băng thông quan trọng cho việc truyền các tensor gradient lớn (chương 6).

Ảo Hóa và Container

  • Ảo hóa chạy nhiều hệ điều hành trên một máy vật lý duy nhất. Một hypervisor (VMware, KVM, Xen) tạo ra các máy ảo (VM), mỗi máy có CPU ảo, bộ nhớ, đĩa và giao diện mạng riêng. Mỗi VM chạy một hệ điều hành hoàn chỉnh (guest OS) tin rằng nó có phần cứng chuyên dụng.

  • VM cung cấp sự cô lập mạnh mẽ (một VM sập không ảnh hưởng đến các VM khác) và tính linh hoạt (chạy Linux và Windows trên cùng một máy, di chuyển VM giữa các máy chủ vật lý). Cái giá là chi phí: mỗi VM chạy một kernel hệ điều hành đầy đủ, tiêu thụ bộ nhớ và CPU cho các thao tác hệ điều hành dư thừa so với host OS.

VM chạy các guest OS riêng trên phần cứng ảo; container chia sẻ kernel của máy chủ và nhẹ hơn nhiều

  • Container (Docker, Podman) cung cấp một giải pháp thay thế nhẹ hơn. Thay vì ảo hóa toàn bộ phần cứng, container chia sẻ kernel của hệ điều hành chủ và sử dụng các tính năng của kernel để cô lập các tiến trình:

    • Namespace cô lập những gì một tiến trình có thể thấy: mỗi container có góc nhìn riêng về cây tiến trình (PID namespace), giao diện mạng (network namespace), điểm gắn kết hệ thống file (mount namespace) và tên máy chủ (UTS namespace). Một tiến trình bên trong container không thể thấy các tiến trình trong container khác.

    • Cgroups (nhóm điều khiển) giới hạn những gì một tiến trình có thể sử dụng: thời gian CPU, bộ nhớ, I/O đĩa, băng thông mạng. Một container không thể tiêu thụ nhiều tài nguyên hơn mức cgroup của nó cho phép, ngăn một container làm chết đói các container khác.

  • Container khởi động trong mili giây (không cần khởi động hệ điều hành), sử dụng chi phí tối thiểu (kernel dùng chung), và được định nghĩa bởi một Dockerfile chỉ định hình ảnh cơ sở, các phụ thuộc và lệnh. Điều này làm chúng có thể tái tạo: docker build tạo ra cùng một môi trường ở mọi nơi.

  • Đối với ML, container giải quyết vấn đề "nó chạy trên máy tôi." Một môi trường huấn luyện với các phiên bản cụ thể của CUDA, cuDNN, PyTorch và Python được đóng gói dưới dạng một image container. Bất kỳ ai cũng có thể tái tạo chính xác môi trường đó trên bất kỳ máy nào. Các nền tảng huấn luyện đám mây (AWS SageMaker, GCP Vertex AI) chạy các công việc huấn luyện trong container.

  • Kubernetes (K8s) điều phối các container ở quy mô lớn: nó lập lịch các container lên một cụm máy tính, khởi động lại các container bị lỗi, mở rộng/thu nhỏ dựa trên tải và quản lý mạng giữa các container. Phục vụ ML quy mô lớn (hàng ngàn bản sao mô hình xử lý hàng triệu yêu cầu) chạy trên Kubernetes.

Kiến Thức Cơ Bản Về Bảo Mật

  • Hệ điều hành thực thi bảo mật qua nhiều cơ chế:

  • Quyền truy cập (Permissions): mọi file có chủ sở hữu, nhóm và các bit quyền (đọc/ghi/thực thi cho chủ sở hữu, nhóm và người khác). Một tiến trình chạy với danh tính (UID) của người dùng đã khởi động nó và chỉ có thể truy cập các file mà bit quyền cho phép. Người dùng root (UID 0) vượt qua mọi kiểm tra quyền, đó là lý do chạy với quyền root rất nguy hiểm.

  • Phân tách đặc quyền (Privilege separation): các tiến trình chạy với các đặc quyền tối thiểu cần thiết. Một máy chủ web không cần truy cập root; nó nên chạy dưới một người dùng bị hạn chế chỉ có thể đọc file web và gắn vào cổng 80. Nếu máy chủ bị xâm phạm, quyền truy cập của kẻ tấn công bị giới hạn trong những gì người dùng bị hạn chế có thể làm.

  • Sandboxing: hạn chế những gì một tiến trình có thể làm ngoài quyền truy cập file. seccomp (Linux) giới hạn các lời gọi hệ thống mà một tiến trình có thể thực hiện. AppArmorSELinux định nghĩa các chính sách kiểm soát truy cập bắt buộc. Container kết hợp namespace, cgroups và seccomp cho sự cô lập nhiều lớp.

  • Ngẫu nhiên hóa bố cục không gian địa chỉ (ASLR): ngẫu nhiên hóa các vị trí bộ nhớ của ngăn xếp, heap và thư viện mỗi khi một chương trình chạy. Điều này làm khó hơn cho kẻ tấn công khai thác các lỗi hỏng bộ nhớ (buffer overflow), vì chúng không thể dự đoán mã hoặc dữ liệu sẽ ở đâu trong bộ nhớ.

  • Bảo mật là mối quan tâm toàn hệ thống: một chuỗi chỉ mạnh bằng mắt xích yếu nhất. Một hệ thống phục vụ mô hình cần giao tiếp mạng an toàn (TLS/HTTPS), truy cập API đã xác thực (API keys, OAuth), xác thực đầu vào (ngăn đầu vào đối nghịch) và thực thi cô lập (container với đặc quyền tối thiểu).

Bài Tập Lập Trình (dùng CoLab hoặc notebook)

  1. Khám phá tạo tiến trình. Sử dụng os.fork() của Python (chỉ Unix) để tạo một tiến trình con và quan sát rằng cả cha và con tiếp tục từ cùng một điểm.

    import os
    
    pid = os.fork()
    
    if pid == 0:
        # Child process
        print(f"Child: my PID is {os.getpid()}, parent PID is {os.getppid()}")
    else:
        # Parent process
        print(f"Parent: my PID is {os.getpid()}, child PID is {pid}")
        os.wait()  # wait for child to finish
    

  2. Mô phỏng lập lịch round-robin. Với một danh sách các tiến trình có thời gian bùng nổ (burst time), mô phỏng lập lịch và tính thời gian chờ trung bình.

    def round_robin(processes, quantum=3):
        """Simulate round-robin scheduling.
        processes: list of (name, burst_time) tuples.
        """
        queue = [(name, burst, 0) for name, burst in processes]  # (name, remaining, wait)
        time = 0
        log = []
    
        while queue:
            name, remaining, waited = queue.pop(0)
            waited += (time - waited - (processes[[p[0] for p in processes].index(name)][1] - remaining))
            run_time = min(quantum, remaining)
            log.append(f"  t={time:3d}: {name} runs for {run_time} (remaining: {remaining - run_time})")
            time += run_time
            remaining -= run_time
    
            if remaining > 0:
                queue.append((name, remaining, time))
            else:
                log.append(f"  t={time:3d}: {name} DONE (turnaround: {time})")
    
        for line in log:
            print(line)
    
    print("Round Robin (quantum=3):")
    round_robin([("P1", 10), ("P2", 4), ("P3", 6)], quantum=3)
    

  3. Mô phỏng thay thế trang với LRU. Với một dãy truy cập trang và một số khung cố định, đếm số lỗi trang.

    def lru_page_replacement(pages, n_frames):
        """Simulate LRU page replacement."""
        frames = []
        faults = 0
    
        for page in pages:
            if page in frames:
                frames.remove(page)
                frames.append(page)  # move to most recently used
                status = "HIT "
            else:
                faults += 1
                if len(frames) >= n_frames:
                    evicted = frames.pop(0)  # remove least recently used
                    status = f"MISS (evict {evicted})"
                else:
                    status = "MISS (cold)"
                frames.append(page)
            print(f"  Page {page}: {status}  frames={frames}")
    
        print(f"\nTotal faults: {faults}/{len(pages)} ({faults/len(pages):.0%})")
    
    print("LRU with 3 frames:")
    lru_page_replacement([1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5], n_frames=3)