Quản lý tiến trình con

Trong nhiều ứng dụng Linux, tiến trình cha cần biết khi nào hoặc tại sao các tiến trình con của nó thay đổi trạng thái (kết thúc hoặc stop bởi signal). Trong bài này, chúng ta sẽ tìm hiểu cách tiến trình cha quản lý trạng thái kết thúc của tiến trình con và các trạng thái của tiến trình con trong Linux.

Quản lý trạng thái tiến trình con

Hệ điều hành Linux cung cấp system call wait() và 1 số hàm tương tự có thể đáp ứng yêu cầu này.

Prototype của wait() như sau:

#include <sys/wait.h>

pid_t wait(int *status);
                        /*Trả về PID của tiến trình con kết thúc, hoặc –1 nếu lỗi*/

System call wait() được gọi trong tiến trình cha để chờ cho đến khi 1 trong các tiến trình con của nó bị kết thúc và trả về trạng thái kết thúc của tiến trình con đó vào con trỏ “status”.

Tại thời điểm wait() được gọi, nếu chưa có tiến trình con nào kết thúc, wait() sẽ block cho đến khi có 1 tiến trình con bị kết thúc. Nếu có 1 tiến trình con đã kết thúc từ trước khi wait() được goi, nó sẽ return ngay lập tức. Nếu con trỏ “status” không NULL, nguyên nhân kết thúc của tiến trình con sẽ được lưu vào số nguyên mà “status” trỏ đến.

System call wait() tồn tại 1 số hạn chế khi nó chỉ có thể theo dõi 1 tiến trình bị kết thúc tiếp theo trong số tất cả các tiến trình con, và sẽ block tiến trình nếu chưa có tiến trình con nào bị kết thúc. Trong nhiều trường hợp chúng ta chỉ muốn theo dõi 1 tiến trình cụ thể. Có 1 system call là waitpid() có thể đáp ứng yêu cầu này với prototype như sau:

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);
                   /*Trả về PID của tiến trình con, 0 (see text), hoặc –1 nếu lỗi*/

Giá trị trả về cũng như biến nguyên nhân kết thúc “status” của waitpid() giống với wait(). Trong đó đối số “pid” xác định tiến trình con mà chúng ta muốn theo dõi, với quy ước như sau:

  • Nếu pid >0, chờ tiến trình con có định danh là pid

  • Nếu pid = 0, chờ bất kỳ tiến trình con nào nằm trong nhóm với tiến trình cha

  • Nếu pid < -1, chờ bất kỳ tiến trình con có process group ID (chúng ta sẽ học ở bài sau) bằng giá trị tuyệt đối với pid. Ví dụ pid ==-200 thì sẽ chờ tiến trình con có pid 200

  • Nếu pid == -1, chờ bất kỳ tiến trình con nào (giống với wait())

Đối số “options” là 1 bit mask có thể OR với 0 hoặc 1 trong các flag WUNTRACED, WCONTINUED, WNOHANG. Chúng ta sẽ không đi sâu thêm vào các flag này tránh làm cho bài học quá rối.

Giá trị “status” chỉ ra nguyên nhân kết thúc của tiến trình con có thể rơi vào 1 trong các trường hợp sau:

  • Tiến trình con kết thúc bình thường bằng hàm exit() hoăc system call _exit()

  • Tiến trình con kết thúc vì nhận 1 signal.

  • Tiến trình con bị dừng bởi 1 signal

  • Tiến trình con tiếp tục bởi SIGCONT signal

Khi lập trình Linux, chúng ta có thể xác định giá trị “status” bằng các macro được cung cấp bởi header <sys/wait.h> như dưới đây:

  • WIFEXITED(status): Return true nếu tiến trình con kết thúc bình thường bằng exit() hoặc _exit()

  • WIFSIGNALED(status): Return true nếu tiến trình con kết thúc bởi 1 signal. Trong trường hợp này có thể dùng thêm macro WTERMSIG(status) để trả về số signal đã kết thúc tiến trình con và macro WCOREDUMP(status) để trả về true nếu tiến trình sinh ra file cordump.

  • WIFSTOPPED(status): Return true nếu tiến trình con bị dừng bởi 1 signal. Có thể dùng thêm macro WSTOPSIG(status) để trả về số signal đã dừng tiến trình con.

  • WIFCONTINUED(status): Return true nếu tiến trình con được phục hồi bởi signal SIGCONT.

Để minh họa rõ hơn về trạng thái của wait() và waitpid(), chúng ta sẽ xét 1 chương trình đơn giản sau đây:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main (void)
{
	int status;
	pid_t pid, childPid;

	/*Tạo tiến trình con bằng fork()*/

	childPid = fork();
	switch(childPid)
	{
		/*fork() trả về lỗi*/
		case -1:
			printf("Error: forkn");
			return -1;
			
		/*Trong tiến trình con, return để kết thúc tiến trình con*/
		case 0:
			return 1;
			
		/*Trong tiến trình cha, dùng wait() để theo dõi con*/
		default:
		pid = wait(&status);

		if (-1 == pid)
		{
			printf("Error, wait" );
		}
		/*In PID của tiến trình con khi wait() return*/
		printf ("pid=%dn" , pid);

		if (WIFEXITED (status))
			printf ("Normal termination with exit status=%dn", WEXITSTATUS (status));

		if (WIFSIGNALED (status))
			printf ("Killed by signal=%d%sn" ,WTERMSIG (status), WCOREDUMP (status) ? " (dumped core)" : "" );

		if (WIFSTOPPED (status))
			printf ("Stopped by signal=%dn", WSTOPSIG (status));

		if (WIFCONTINUED (status))
			printf ("Continuedn" );

	}
	return 0;
}

Trong chương trình trên, tiến trình con kết thúc bình thường bằng cách gọi return 1, vì vậy, compile và chạy chương trình trên sẽ được kết quả sau:

Bây giờ, thay vì gọi “return 1” trong tiến trình con, chúng ta gọi abort() để hệ thống gửi signal SIGABRT đến chương trình con, chúng ta sẽ được kết quả sau:

Kết quả trên cho thấy tiến trình con đã bị kết thúc bởi 1 signal có số signal 6, và file coredump đã được sinh ra.

Ngoài 2 system call wait() và waitpid(), chúng ta cũng có thể sử dụng một số system call khác là:

  • waittid()

  • wait3()

  • wait4()

Các system call này không những có thể thực hiện đủ công việc của waitpid() mà còn có thể cung cấp thêm 1 số thông tin về tài nguyên của tiến trình con. Chúng ta không thể đi qua tất cả các system call liên quan đến chờ tiến trình con mà chỉ cần hiểu bản chất của chúng là gì.

Tiến trình orphan và tiến trình zombie

Như đã nói ở trên, tiến trình cha có thể gọi system call wait() để xác nhận trạng thái kết thúc của tiến trình con. Tuy nhiên, có nhiều trường hợp tiến trình cha bị kết thúc trước tiến trình con, điều này có thể do tiến trình cha bị crash hoặc lập trình viên không kết thúc tiến trình con trước khi kết thúc tiến trình cha. Hoặc có trường hợp tiến trình con kết thúc trước khi tiến trình cha gọi wait(). Hai trường hợp này sẽ giới thiệu thuật ngữ trạng thái mới của tiến trình con là orphan và zombie,

  • Orphan process: Dịch nôm na là tiến trình mồ côi, là trạng thái của tiến trình con khi tiến trình cha kết thúc trong khi tiến trình con vẫn tồn tại. Lúc này tiến trình con sẽ ở trạng thái “mồ côi”. Trong Linux, sau khi tiến trình con “mồ côi”, tiến trình init vốn là tiến trình đầu tiên và là tiến trình “tổ tiên” của mọi tiến trình sẽ nhận tiến trình con đó làm “con”. Lúc này, nếu gọi system call getppid() trong tiến trình con, nó sẽ return giá trị “1” là PID của tiến trình init.

  • Zombie process: Dịch tạm là tiến trình thây ma, là trạng thái của tiến trình con khi nó kết thúc mà tiến trình cha chưa gọi wait(). Quan điểm của HĐH Linux là khi tiến trình con kết thúc, thay vì biến mất hoàn toàn thì tiến trình cha vẫn có thể gọi wait() để lấy trạng thái kết thúc của tiến trình con sau đó. Vì vậy, kernel đã chuyển tiến trình con thành trạng thái zombie process. Lúc này hầu hết các tài nguyên của tiến trình con đã được thu hồi và có thể sử dụng cho các tiến trình khác nhưng vẫn còn một số thông tin cơ bản của tiến trình con được giữ lại (PID, trạng thái kết thúc và một số chỉ số của tiến trình). Những thông tin này sẽ được sử dụng để trả về khi tiến trình cha gọi wait() và sau đó sẽ được giải phóng hoàn toàn bởi kernel.

Khi sử dụng command ps, tiến trình zombie sẽ được nhận biết bằng trạng thái “Z+” hoặc ký hiệu <defunct> như sau:

Tiến trình zombie đúng với cảm hứng của zombie trong các bộ phim chúng ta thường xem: đã bị thu hồi tài nguyên (đã chết) nhưng vẫn chưa biến mất hoàn toàn mà vẫn tồn tại (ở dạng thây ma). Vì đã là zombie, nên nó không thể bị giết lại lần nữa và vẫn chiếm dụng tài nguyên PID. Lúc này chỉ còn cách là kill tiến trình cha để tiến trình init nhận tiến trình thây ma là con, sau đó tiến trình init sẽ tự động gọi wait() để giải phóng hoàn toàn tiến trình con. Tiến trình zombie thường không phải là vấn đề lớn với hệ thống nếu chỉ có 1 vài cái, vì tài nguyên nó chiếm dụng là khá ít. Tuy nhiên, vì nó vẫn chiếm dụng PID, nên nếu có quá nhiều tiến trình zombie có thể làm đầy bảng PID của kernel và sẽ không thể tạo ra tiến trình mới.

Kết luận

Sau bài này chúng ta đã nắm được cách Linux cung cấp các system call cho tiến trình cha để theo dõi trạng thái của tiến trình con nhằm đảm bảo hệ thống quản lý hoạt động cũng như không gian bộ nhớ cho các tiến trình chặt chẽ hơn. Ngoài ra, chúng ta cũng cần nắm được khái niệm về các trạng thái của tiến trình con là orphan process và zombie process.

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *