Tiến trình – process là một khái niệm cơ bản trong bất kì một hệ điều hành nào. Một tiến trình có thể được định nghĩa là một thực thể chương trình đang được chạy trong hệ thống. Một web server chạy trong thiết bị là một tiến trình, hoặc một chương trình soạn thảo văn bản đang chạy trong thiết bị cũng là một tiến trình.
Một ví dụ khác: Cùng là một chương trình soạn thảo (ví dụ chương trình quen thuộc vim), người dùng mở 16 chương trình vim để thao tác với 16 file khác nhau, chúng ta cho 16 tiến trình chạy trong hệ thống.
Bài này sẽ giới thiệu về tiến trình (process), tiến trình nhẹ (lightweight process) và luồng (thread) trong Linux.
Tiến trình – Process
Như đã đề cập ở trên, tiến trình là một thực thể chương trình đang được thực thi. Nó là một tập hợp các cấu trúc dữ liệu mô tả đầy đủ quá trình một chương trình được chạy trong hệ thống.
Một tiến trình cũng trải qua các quá trình như con người: Nó được sinh ra, nó có thể có một cuộc đời ít ý nghĩa hoặc nhiều ý nghĩa, nó có thể sinh ra một hoặc nhiều tiến trình con, và thậm chí, nó có có thể chết đi. Điều khác biệt nhỏ duy nhất là: tiến trình không có giới tính. Mỗi tiến trình chỉ có một tiến trình cha (hoặc có thể gọi là mẹ, ở trong khóa học sẽ thống nhất gọi là cha J) duy nhất.
Dưới góc nhìn của kernel, tiến trình là một thực thể chiếm dụng tài nguyên của hệ thống (Thời gian sử dụng CPU, bộ nhớ, …)
Khi một tiến trình con được tạo ra, nó hầu như giống hệt như tiến trình cha. Tiến trình con sao chép toàn bộ không gian địa chỉ, thực thi cùng một mã nguồn như tiến trình cha, và bắt đầu chạy tiếp những mã nguồn riêng cho tiến trình con từ thời điểm gọi hàm tạo tiến trình mới.
Mặc dù tiến trình cha và tiến trình con cùng chia sẻ sử dụng phần mã nguồn của chương trình, nhưng chúng lại có phần dữ liệu tách biệt nhau (stack và heap). Điều này có nghĩa là, những sự thay đổi dữ liệu của tiến trình con không ảnh hưởng đến dữ liệu trong tiến trình cha.
Một ví dụ để làm rõ khái niệm này:
#include <unistd.h> #include <sys/types.h> #include <errno.h> #include <stdio.h> #include <sys/wait.h> #include <stdlib.h> int main() { pid_t child_pid; int counter = 2; printf(“Gia tri khoi tao cua counter = %dn”, counter); child_pid = fork(); if(child_pid >= 0) { if(0 == child_pid) { counter ++; sleep(2); printf(“Day la tien trinh con, gia tri counter sau khi tang = %dn”, counter); while(1); } else { counter ++; sleep(5); printf(“Day la tien trinh cha, gia tri counter sau khi tang = %dn”, counter); while(1); } } else { printf(“Tao process con khong thanh congn”); } return 0; }
Kết quả chương trình khi chạy:
Trong ví dụ trên, tiến trình con được tạo ra từ tiến trình cha. Bằng câu lệnh ps trong linux, ta sẽ thấy có 2 tiến trình tên là test được chạy trong hệ thống.
Tiến trình con được kế thừa biến counter từ tiến trình cha. Tuy nhiên sự thay đổi biến counter ở cả 2 tiến trình cha và con đều không ảnh hưởng tới nhau.
Luồng – User threads
Luồng – User thread (phân biệt với kernel thread) là một luồng thực thi mã nguồn trong một tiến trình. Một tiến trình bao gồm nhiều luồng thực thi như thế được gọi là một tiến trình đa luồng – multithreaded process.
Các threads trong một tiến trình chỉ có ý nghĩa đối trong tiến trình đó (không thể nhìn thấy từ bên ngoài), và chúng chia sẻ dữ liệu sử dụng trong tiến trình. Chính vì thế, khác với tiến trình cha – con, sự thay đổi dữ liệu ở luồng này sẽ ảnh hưởng tới những luồng còn lại.
Một ví dụ về luồng: Một ứng dụng chơi cờ đơn giản khi chạy cũng là một tiến trình đa luồng. Khi ứng dụng chạy, ứng dụng sẽ phải vừa chờ đợi các nước đi của người chơi để thể hiện trên hình ảnh (đây sẽ là một luồng), vừa phải tính toán các nước đi tiếp theo của máy để chiến thắng được người chơi (đây là luồng thứ 2). Hai luồng này chia sẻ dữ liệu chung là nước đi của người chơi, nhưng vẫn hoạt động song song và thực hiện 2 công việc khác nhau.
Tuy nhiên, dưới góc nhìn của kernel, một tiến trình đa luồng cũng giống hệt một tiến trình bình thường khác. Ví dụ chúng ta đang chạy 3 tiến trình, 2 tiến trình 1 luồng, 1 tiến trình 3 luồng. Khi kernel lập lịch chạy cho 3 tiến trình đó, tiến trình có 3 luồng vẫn chỉ có thể chiếm 1/3 thời gian hoạt động của CPU, giống như 2 tiến trình còn lại. Điều này có nghĩa là, việc tạo ra, quản lý và lập lịch cho các luồng trong một tiến trình sẽ được thực hiện hoàn toàn ở user space.
Ngày nay, hầu hết các tiến trình đa luồng được viết bởi thư viện chuẩn pthread (POSIX thread).
Một ví dụ về chương trình đa luồng:
#include <stdio.h> #include <pthread.h> #include <semaphore.h> #include <stdlib.h> pthread_mutex_t lock= PTHREAD_MUTEX_INITIALIZER; int counter = 2; void* thread1() { sleep(1); pthread_mutex_lock(&lock); counter ++; printf(“Day la thread 1: gia tri counter = %dn”, counter); pthread_mutex_unlock(&lock); while(1); } void* thread2() { sleep(3); pthread_mutex_lock(&lock); counter ++; printf(“Day la thread 2: gia tri counter = %dn”, counter); pthread_mutex_unlock(&lock); while(1); } int main() { pthread_t th1, th2; pthread_create(&th1,NULL,thread1,NULL); pthread_create(&th2,NULL,thread2,NULL); pthread_join(th1,NULL); pthread_join(th2,NULL); return 0; }
Trong ví dụ này, tiến trình test tạo ra 2 luồng.
Hai luồng cùng sử dụng chung biến counter. Khi có sự thay đổi giá trị của biến counter tại một luồng, thì luồng còn lại cũng sẽ “nhận biết” được sự thay đổi đó.
Kết quả khi chạy chương trình:
Tiến trình nhẹ – Lightweight process
Như đề cập ở phần trên, tiến trình đa luồng được kernel nhìn giống hệt như các tiến trình đơn luồng khác, nên kernel sẽ cung cấp các tài nguyên của hệ thống (thời gian sử dụng CPU) giống nhau giữa các tiến trình đa luồng và tiến trình đơn luồng. Điều nay khiến cho các tiến trình đa luồng trở nên không thực sự tốt.
Trở lại ví dụ ứng dụng chơi cờ đề cập ở phần trên. Trong khi luồng thứ nhất (chờ đợi các nước đi của người chơi để thể hiện trên hình ảnh) được thực hiện, thì luồng thứ hai (tính toán các nước đi tiếp theo của máy) nên được chạy một cách liên tục, tận dụng khoảng thời gian suy nghĩ của người chơi để đưa ra các nước đi tốt nhất.
Tuy nhiên, giả sử luồng thứ nhất thực thi một blocking system call nào đó, thì luồng thứ hai cũng sẽ bị block lại theo. Lý do bởi vì kernel coi một tiến trình đa luồng giống như một tiến trình đơn luồng bình thường khác, nên khi luồng thứ nhất thực hiện một blocking system call, kernel sẽ coi đó là hành động của cả tiến trình và block cả tiến trình đó lại. Do đó, tiến trình đa luồng trở nên không tối ưu cho ứng dụng.
Linux sử dụng tiến trình nhẹ – lightweight process để cung cấp phương thức thực hiện tối ưu hơn cho một ứng dụng đa luồng. Về cơ bản, hai tiến trình nhẹ có thể chia sẻ và sử dụng cùng một tài nguyên của hệ thông như không gian địa chỉ, files … Khi một tiến trình nhẹ thay đổi tài nguyên dùng chung, tiến trình nhẹ còn lại có thể nhìn thấy sự thay đổi đó.
Tiến trình nhẹ là một tiến trình, nhưng thay vì có không gian địa chỉ, tài nguyên riêng biệt như khái niệm tiến trình bình thường, chúng có thể chia sẻ sử dụng chung tài nguyên với nhau như khái niệm của luồng trên user space.
Điểm khác biệt giữa tiến trình nhẹ và luồng là việc kernel nhìn tiến trình nhẹ như một tiến trình bình thường, nên kernel sẽ cung cấp tài nguyên hệ thống, lập lịch làm việc cho tiến trình nhẹ. Nên nếu hệ thống đang chạy hai tiến trình, một tiền trình đơn luồng, một tiến trình đa luồng gồm hai tiến trình nhẹ, thì tiến trình đa luồng đó sẽ chiếm 2/3 thời gian sử dụng CPU.
Một cách cơ bản để tạo ra một ứng dụng đa luồng là gán mỗi một luồng với một tiến trình nhẹ. Có nhiều thư viện tạo luồng sử dụng phương thức này ví dụ như: LinuxThreads, Native POSIX Thread Library (NPTL) …
Quay trở lại lần nữa với ví dụ về ứng dụng chơi cờ: Nếu gán hai luồng của ứng dụng vào hai tiến trình nhẹ, thì dùng luồng một (chờ đợi các nước đi của người chơi để thể hiện trên hình ảnh) có thực thi một blocking system call, thì luồng thứ hai (tính toán các nước đi tiếp theo của máy) sẽ vẫn được thực hiện liên tục do kernel sẽ lập lịch làm việc cho luồng thứ hai.
Hiển thị tiến trình nhẹ: Chạy lại chương trình ví dụ với luồng bên trên và dùng câu lệnh ps với tùy chọn “-fL -C”. Cột LWP hiển thị số tiến trình nhẹ mà tiến trình test đang có. Tiến trình test sẽ có 3 tiến trình nhẹ, một là tiến trình chính, và hai tiến trình nhẹ con tương ứng với hai luồng con được tạo ra (thread1 và thread2)
Mối quan hệ giữa tiến trình – luồng – tiến trình nhẹ:
Hình vẽ bên trên mô tả mối quan hệ giữa tiến trình – luồng – tiến trình nhẹ. Một tiến trình chạy trong hệ thống có thể gồm nhiều luồng (user threads), một luồng này được liên kết trực tiếp 1-1 với một tiến trình nhẹ, hoặc nhiều luồng có thể liên kết nhiều – 1 với một tiến trình nhẹ. Việc thực thi, lập lịch cho các tiến trình nhẹ được thực hiện bởi kernel.
Sau bài này, các bạn đã có những khái niệm về tiến trình, luồng và tiến trình nhẹ trong Linux. Bài tiếp theo sẽ tìm hiểu sâu hơn những trạng thái của tiến trình, và làm cách nào Kernel quản lý các tiến trình trong hệ thống.
Tôi là Nguyễn Văn Sỹ có 15 năm kinh nghiệm trong lĩnh vực thiết kế, thi công đồ nội thất; với niềm đam mê và yêu nghề tôi đã tạo ra những thiết kếtuyệt vời trong phòng khách, phòng bếp, phòng ngủ, sân vườn… Ngoài ra với khả năng nghiên cứu, tìm tòi học hỏi các kiến thức đời sống xã hội và sự kiện, tôi đã đưa ra những kiến thức bổ ích tại website nhaxinhplaza.vn. Hy vọng những kiến thức mà tôi chia sẻ này sẽ giúp ích cho bạn!