Vietnamese Developers' Blog

Viết daemon trên Linux (1)

Posted in Unix/Linux/BSD by Hoang Tran on the February 25th, 2008

1. Daemon là gì?

Một daemon (hay service) là một background process được thiết kế để chạy độc lập, rất ít hoặc không có sự can thiệp của user. Daemon http của Apache web server là một ví dụ về daemon. Nó chạy ở dưới background, lắng nghe một số port xác định và cung cấp các pages hoặc processes scripts dựa vào kiểu request của user.

Để tạo một daemon trong Linux, chúng ta cần phải thực hiện một số bước theo thứ tự. Hiểu sâu bên trong cách một daemon hoạt động còn giúp chúng ta hiểu các hàm system call của kernel của Linux. Thực tế thì trong kernel module, có nhiều daemon quản lý các hardware device như là các mạch điều khiển ngoài, printer và PDAs. Chúng là một trong những khối cơ bản trên Linux mà nó cung cấp sự mềm dẻo và sức mạnh tuyệt vời.

Thông qua tài liệu này, chúng ta sẽ thể hiện một daemon rất đơn giản được viết bằng C. Như bạn có thể theo dõi từng bước, các đoạn mã sẽ được thêm vào chỉ ra thứ tự thực hiện các bước để thiết lập daemon và chạy nó.

2. Khởi đầu

Đầu tiên, bạn cần phải có những package sau được cài đặt trên Linux để phát triển các daemons:

- GCC 3.2.2 or higher

- Linux Development headers and libraries

Nếu hệ thống của bạn không có những package này bạn cần cài đặt nó. Để tìm version mà GCC bạn đang cài đặt, sử dụng lệnh:

gcc –-version

3. Thiết kế

3.1 Nó sẽ làm gì?

Một daemon sẽ làm một việc nào đó và cần làm nó tốt. Nó có thể phức tạp như phải quản lý hàng trăm mailbox với rất nhiều domains, hoặc là đơn giản như việc viết một report và gọi sendmail để gửi report đó đi.

Trong bất kỳ trường hợp nào, bạn cần phải lên kế hoạch xem daemon sẽ làm gì. Nếu nó cần tương tác với các daemon khác (có thể bạn chưa viết) thì đó sẽ là điều cần xem xét.

3.2 Tương tác như thế nào

Daemon không nên trao đổi trực tiếp với user thông qua terminal. Thực tế, một daemon hoàn toàn không nên tương tác trực tiếp với user. Tất cả sự trao đổi nên thông qua một dạng giạo diện mà nó có thể phức tạp như một GTK+ GUI hay đơn giản như là một tập các tín hiệu.

4. Cấu trúc cơ bản của daemon

Khi một daemon khởi động, nó phải thực hiện một vài công việc ở mức thấp (low-level) để sẵn sàng thực hiện các công việc thực sự. Nó sẽ cần thực hiện các bước sau:

• Fork off the parent process

• Change file mode mask (umask)

• Open logs for writing

• Create a unique Session ID (SID)

• Change the current working directory to a safe place

• Close standard file descriptors

• Enter actual daemon code


4.1 Forking The Parent Process

Daemon được khởi động hoặc bởi bản thân hệ thống hoặc bởi user command hay script. Khi nó bắt đầu chạy, thì nó được cấp một process và giống như những file có thể chạy khác trên hệ thống. Để cho nó thực sự dưới background, một tiến trình con cần phải được tạo ra tại nơi mà đoạn mã thực sự được thực thi. Quá trình này được biết như là forking và sử dụng hàm fork(). Hãy tham khảo blog trước về fork().

pid_t pid;
 
/* Fork off the parent process */
pid = fork();
if(pid < 0) {
  exit(EXIT_FAILURE);
}
 
/* If we got a good PID, then
    we can exit the parent process. */
if (pid > 0) {
  exit(EXIT_SUCCESS);
}

Chú ý rằng chúng ta kiểm tra lỗi ngay sau lệnh fork(). Khi viết một daemon, bạn sẽ phải viết những đoạn mã được bảo vệ bao nhiêu càng tốt bấy nhiêu. Thực tế thì một điều tốt là một phần trong toàn bộ các đoạn mã của daemon không có tác dụng gì ngoài chức năng kiểm tra lỗi.

Hàm fork() trả về hoặc là process id (PID) của tiến trình con (khác 0) hoặc là -1 nếu lỗi. Nếu process không thể fork một child process thì daemon nên tạm dừng tại đây.

Nếu fork() thành công, thì parent process phải thoát êm đẹp. Điều này khá kỳ lạ với những người không nhìn thấy nó, nhưng bởi vì forking thì child process sẽ tiếp tục thực thi từ đây trong đoạn mã.

4.2 Thay đổi mặt lạ file (Umask)

Để viết vào bất kỳ file nào (bao gồm file logs) được tạo ra bởi daemon, thì mặt lạ file (umask) phải được thay đổi để đảm bảo rằng chúng có thể được viết hay đọc chính xác. Điều này tương tự với việc chạy umask từ command line, nhưng ta thực hiện nó bằng hàm umask() ở đây:

pid_t pid, sid;
 
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
  /* Log failure (use syslog if possible) */
  exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
  exit(EXIT_SUCCESS);
}
 
/* Change the file mode mask */
umask(0);

Bằng việc thiết lập umask bằng 0, chúng ta có toàn quyền truy nhập vào những file được tạo ra bởi daemon. Thậm chí nếu bạn không có kế hoạch sử dụng những file này thì nó vẫn là ý kiến hay khi thiết lập umask ở đây, trong trường hợp bạn đang truy nhập những file khác trong hệ thống file.

4.3 Mở logs để ghi

Phần này là tùy chọn, nhưng khuyến nghị rằng bạn nên mở một log file ở đâu đó trong hệ thống để theo dõi hệ thông. Nó có thể ở nơi mà bạn có thể nhìn vào các thông tin gỡ lỗi cho daemon của bạn.

4.4 Khởi tạo Unique Session ID (SID)

Từ đây, child process phải thiết lập một unique SID từ kernel để hoạt động. Trong khi đó, child process phải trở thành một “đứa trẻ mồ côi” (parent process died) trong hệ thống. Kiểu pid_t được khai báo trong phần trước cũng được sử dụng để tạo một SID mới cho child process.

pid_t pid, sid;
 
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
 exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
  exit(EXIT_SUCCESS);
}
 
/* Change the file mode mask */
umask(0);
 
/* Open any logs here */
 
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
  /* Log any failure */
  exit(EXIT_FAILURE);
}

Một lần nữa, hàm setsid() có cùng giá trị trả về như hàm fork(). Chúng ta có thể áp dụng cùng một cách thức kiểm tra lỗi ở đây cho hàm khởi tạo SID cho child process.

4.5 Thay đổi thư mục làm việc

Thư mục làm việc hiện tại nên được chuyển tới những nơi cố định. Bởi vì rất nhiều bản phân phối Linux không hoàn toàn tuân theo chuẩn Linux Filesystem Hierarchy, thư mục root (/) là thư mục duy nhất được đảm bảo theo chuẩn. Chúng ta có thể làm việc đó bằng cách sử dụng hàm chdir():

pid_t pid, sid;
 
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
 exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
 exit(EXIT_SUCCESS);
}
 
/* Change the file mode mask */
umask(0);
 
/* Open any logs here */
 
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
 /* Log any failure here */
 exit(EXIT_FAILURE);
}
 
/* Change the current working directory */
if ((chdir("/")) < 0) {
 /* Log any failure here */
 exit(EXIT_FAILURE);
}

Một lần nữa bạn lại có thể nhìn thấy những đoạn mã bảo vệ. Hàm chdir() trả về -1 nếu lỗi, do đó để chắc chắn chúng ta cần kiểm tra giá trị đó sau khi chuyển đến thư mục root.

4.6 Đóng những mô tả file chuẩn

Một trong những bước cuối cùng trong việc thiết lập một daemon là đóng những mô tả file chuẩn (stdin, stdout, stderr). Bởi vì một daemon phải không được nhìn thấy terminal, những mô tả phải này trở nên thừa thãi và có thể là những mối nguy hiểm về bảo mật.

Hàm close() có thể thực hiện việc này:

pid_t pid, sid;
 
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
 exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
 exit(EXIT_SUCCESS);
}
 
/* Change the file mode mask */
umask(0);
 
/* Open any logs here */
 
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
  /* Log any failure here */
  exit(EXIT_FAILURE);
}
 
/* Change the current working directory */
if ((chdir("/")) < 0) {
  /* Log any failure here */
  exit(EXIT_FAILURE);
}
 
/* Close out the standard file descriptors */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

Đó là một ý tưởng tốt khi gắn những hằng tự định nghĩa cho các mô tả file, để tăng tính portable giữa các hệ thống khác nhau.

5. Viết mã cho daemon

5.1 Khởi tạo

Tại thời điểm này, về cơ bản chúng đã nói cho Linux rằng đó là một daemon, do đó bây giờ là lúc viết những đoạn mã thực sự cho daemon. Khởi tạo (initialization) là bước đầu tiên. Vì ở đây có rất nhiều hàm khác nhau có thể gọi để thiết lập các nhiệm vụ cho daemon nên tôi sẽ không đi xa hơn ở đây.

Nhưng điểm lớn nhất ở đây là khi khởi tạo bất kỳ thứ gì cho daemon, cần phải áp dụng quy tắc bảo vệ cho daemon giống như ở phần trước.

Hãy càng chi tiết càng tốt khi viết vào syslog hay những file logs của bạn. Việc gỡ lỗi daemon có thể khá khó khi không có đủ thông tin cần thiết về trạng thái của daemon.

5.2 Vòng lặp

Đoạn mã chính của daemon nằm trong vòng lặp vô tận. Về mặt kỹ thuật nó không phải là một vòng lặp vô tận nhưng nó được cấu tạo như sau:

pid_t pid, sid;
 
/* Fork off the parent process */
pid = fork();
if (pid < 0) {
  exit(EXIT_FAILURE);
}
/* If we got a good PID, then
we can exit the parent process. */
if (pid > 0) {
  exit(EXIT_SUCCESS);
}
 
/* Change the file mode mask */
umask(0);
 
/* Open any logs here */
 
/* Create a new SID for the child process */
sid = setsid();
if (sid < 0) {
  /* Log any failures here */
  exit(EXIT_FAILURE);
}
 
/* Change the current working directory */
if ((chdir("/")) < 0) {
  /* Log any failures here */
  exit(EXIT_FAILURE);
}
 
/* Close out the standard file descriptors */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
 
/* Daemon-specific initialization goes here */
 
/* The Big Loop */
while (1) {
  /* Do some task here ... */
  sleep(30); /* wait 30 seconds */
}

Vòng lặp tiêu biểu thường là một vòng lặp while mà có một điều kiện kết thúc vô tận với lời gọi tới hàm sleep bên trong để làm cho nó chạy với những khoảng cụ thể.

Hãy nghĩ về nó như nhịp đập tim: khi tim bạn đập, nó thực hiện một vài tác vụ, sau đó chờ đợi cho đến nhịp đập tiếp theo diễn ra. Rất nhiều daemons thực hiện theo phương thức này.

6. Tổng hợp

6.1 Ví dụ hoàn chỉnh

Ví dụ dưới đây là một daemon ví dụ hoàn chỉnh mà chỉ ra tất cả các bước cần thiết cho việc thiết lập và thực thi daemon. Để chạy nó, đơn giản chỉ cần biên dịch sử dụng gcc và thực thi từ command line. Để kết thúc nó, hay sử dụng lệnh kill sau khi tìm thấy PID của nó.

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <string.h>
 
int main(void) {
 
	/* Our process ID and Session ID */
	pid_t pid, sid;
 
	/* Fork off the parent process */
	pid = fork();
	if (pid < 0) {
	  exit(EXIT_FAILURE);
	}
	/* If we got a good PID, then
	we can exit the parent process. */
	if (pid > 0) {
	  exit(EXIT_SUCCESS);
	}	
 
	/* Change the file mode mask */
	umask(0);
 
	/* Open any logs here */
 
	/* Create a new SID for the child process */
	sid = setsid();
	if (sid < 0) {
	  /* Log the failure */
	  exit(EXIT_FAILURE);
	}
 
	/* Change the current working directory */
	if ((chdir("/")) < 0) {
	  /* Log the failure */
	  exit(EXIT_FAILURE);
	}
 
	/* Close out the standard file descriptors */
	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);
 
	/* Daemon-specific initialization goes here */
 
	/* The Big Loop */
	while (1) {
	  /* Do some task here ... */
	  sleep(30); /* wait 30 seconds */
	}
 
	exit(EXIT_SUCCESS);
}

Từ đây, bạn có thể sử dụng bộ khung này để viết daemons của chính bạn. Hãy chắc chắn thêm vào quá trình log của bạn (hoặc sử dụng chức năng của syslog). Cuối cùng hãy chú viết những đoạn mã an toàn bằng việc kiểm tra lỗi trả về thường xuyên.

Tagged with: ,

6 Responses to 'Viết daemon trên Linux (1)'

Subscribe to comments with RSS or TrackBack to 'Viết daemon trên Linux (1)'.

  1. Kiên said, on June 26th, 2008 at 3:53 pm

    Bác Hoàng viết nốt cách connect đến 1 daemon xem nào.

  2. Viết daemon trên Linux (2) said, on July 3rd, 2008 at 1:37 pm

    [...] Bài viết này bổ xung một số vấn đề chưa được trình bày trong bài viết “Viết daemon trên Linux (1)” của anh [...]

  3. bronzeboyvn said, on June 12th, 2009 at 4:40 am

    Mình không biết khái niệm Unique Session ID, mong mọi người giải thích cụ thể hay cho tài liệu (tự đọc) cung duoc.

  4. kiennguyen said, on June 14th, 2009 at 5:29 am

    @bronzeboyvn: Bác Hoàng viết thế này thì đánh đố nhau quá :D Mình cũng ko hiểu rõ cái này lắm, nhưng sau một hồi nghiên cứu thì hình như là thế này:

    - Trong Unix thì các process thường được nhóm lại với nhau thành các processes group.

    - Các processes groups lại được nhóm lại thành các process sessions.

    - Các process trong một session thì chia sẻ cùng một controlling terminal, thường là terminal người dùng sử dụng để khởi động process, và sau đó dùng để tương tác với các process (ví dụ ấn Ctrl-C để kill).

    - Tuy nhiên 1 daemon thì không nên tương tác với người dùng thông qua terminal. Bởi vậy chúng ta dùng hàm setsid() để đưa daemon vào một session hoàn toàn mới không liên quan đến controlling terminal nào cả.

    Mình tìm hiểu qua thì thấy như vậy, chi tiết thế nào chắc phải chờ tác giả bài viết vào trả lời :)

    Bạn có thể search các từ khoá controlling terminal hay process group, process session hoặc xem trong chương 9 cuốn Advanced Programming in The Unix Environment của Stevens & Rago.

    Cheers!

  5. bronzeboyvn said, on June 14th, 2009 at 7:16 am

    cám ơn kiennguyen đã cho ít thông tin về SID, giúp mình hiểu tại sao cần gọi hàm setsid() ở đây. Cuốn Beginning Linux Programming mình lyện chưa thông, chuyển lên advanced dễ sock thuốc lắm. Dù gì có người nhắc bài thì luyện lẹ hơn.
    Cái cách nó nghĩ ra process group thì mình cũng biết tại sao.
    /*
    Mọi thời điểm trong OS chỉ có 1 foreground job. Shell nó sẽ tạo mỗi process group cho 1 job. Thường process group ID (gid = ID cua 1 trong những processes cha trong group. Còn lại trong nhóm là các processes con. Điều này thuận lợi cho việc gởi tín hiệu tới processes, ta có thể gởi 1 signal tới cả group. Ví dụ lệnh kill:
    # kill -9 12345 // goi SIGKILL toi process 12345
    #kill -9 -12345 // goi SIGKILL toi ca process group 12345.
    Thuan loi khi ta don sach khoi he thong 1 job nao do
    */
    Chứ còn nhóm các groups lại 1 sesion, thì mình không hiểu tại sao trong OS thiết kế như vậy.
    Bắt đầu tuần này vua coi confederations cup, vua luyện cho xong cái POSIX thread. Chắc chắn sẽ có nhiều vấn đề cần giải thích thêm.

  6. kiennguyen said, on June 15th, 2009 at 4:51 am

    Cuốn đó tên là Advanced nhưng nội dung thì rất basic :) Đó là cuốn sách rất tốt để tra cứu vì nó có gần như tất cả những gì liên quan đến Unix programming.

    Công nhận lúc chưa đi làm là lúc tốt nhất để học. Đi làm rồi không còn thời gian đọc sách nữa.

Leave a Reply