Linux系統程式設計——守護程序
- 1. 終端
-
- 1.1 終端的啟動流程
- 1.2 ttyname函數
- 1.3 網絡終端
- 2. 程序組
-
- 2.1 概念和特性
- 2.2 程序組相關函數
- 3. 會話
-
- 3.1 建立會話
- 3.2 getsid函數
- 3.3 setsid函數
- 4. 守護程序
1. 終端
輸入裝置和輸出裝置的綜合稱為終端
在UNIX系統中,使用者通過終端登入系統後得到一個Shell程序,這個終端成為Shell程序的控制終端(Controlling Terminal),程序中,控制終端是儲存在PCB中的資訊,而fork會複制PCB中的資訊,是以由Shell程序啟動的其它程序的控制終端也是這個終端。預設情況下(沒有重定向),每個程序的标準輸入、标準輸出和标準錯誤輸出都指向控制終端,程序從标準輸入讀也就是讀使用者的鍵盤輸入,程序往标準輸出或标準錯誤輸出寫也就是輸出到顯示器上。信号中還講過,在控制終端輸入一些特殊的控制鍵可以給前台程序發信号,例如Ctrl-C表示SIGINT,Ctrl-\表示SIGQUIT。
- Alt + Ctrl + F1、F2、F3、F4、F5、F6 字元終端 pts (pseudo terminal slave) 指僞終端
- Alt + F7 圖形終端
- SSH、Telnet… 網絡終端
1.1 終端的啟動流程
檔案與I/O中講過,每個程序都可以通過一個特殊的裝置檔案
/dev/tty
通路它的控制終端。事實上每個終端裝置都對應一個不同的裝置檔案,/dev/tty提供了一個通用的接口,一個程序要通路它的控制終端既可以通過/dev/tty也可以通過該終端裝置所對應的裝置檔案來通路。ttyname函數可以由檔案描述符查出對應的檔案名,該檔案描述符必須指向一個終端裝置而不能是任意檔案。
簡單來說,一個Linux系統啟動,大緻經曆如下的步驟:
init --> fork --> exec --> getty --> 使用者輸入帳号 --> login --> 輸入密碼 --> exec --> bash(文字終端)
硬體驅動程式負責讀寫實際的硬體裝置,比如從鍵盤讀入字元和把字元輸出到顯示器,線路規程像一個過濾器,對于某些特殊字元并不是讓它直接通過,而是做特殊處理,比如在鍵盤上按下Ctrl-z,對應的字元并不會被使用者程式的read讀到,而是被線路規程截獲,解釋成SIGTSTP信号發給前台程序,通常會使該程序停止。線路規程應該過濾哪些字元和做哪些特殊處理是可以配置的。
line disciline: 線路規程,用來過濾鍵盤輸入的内容。
1.2 ttyname函數
由檔案描述符查出對應的檔案名
char *ttyname(int fd);
成功:終端名;失敗:NULL,設定errno
借助ttyname函數,通過實驗看一下各種不同的終端所對應的裝置檔案名。
#include <unistd.h>
#include <iostream>
using namespace std;
int main(void)
{
cout << "fd 0:" << ttyname(0) << endl;
cout << "fd 1:" << ttyname(1) << endl;
cout << "fd 2:" << ttyname(2) << endl;
return 0;
}
1.3 網絡終端
虛拟終端或序列槽終端的數目是有限的,虛拟終端(字元控制終端)一般就是/dev/tty1∼/dev/tty6六個,序列槽終端的數目也不超過序列槽的數目。然而網絡終端或圖形終端視窗的數目卻是不受限制的,這是通過僞終端(Pseudo TTY)實作的。
一套僞終端由一個主裝置(PTY Master)和一個從裝置(PTY Slave)組成。主裝置在概念上相當于鍵盤和顯示器,隻不過它不是真正的硬體而是一個核心子產品,操作它的也不是使用者而是另外一個程序。從裝置和上面介紹的/dev/tty1這樣的終端裝置子產品類似,隻不過它的底層驅動程式不是通路硬體而是通路主裝置。網絡終端或圖形終端視窗的Shell程序以及它啟動的其它程序都會認為自己的控制終端是僞終端從裝置,例如/dev/pts/0、/dev/pts/1等。下面以telnet為例說明網絡登入和使用僞終端的過程。
網絡終端
TCP/IP協定棧:在資料包上添加報頭。
如果telnet用戶端和伺服器之間的網絡延遲較大,我們會觀察到按下一個鍵之後要過幾秒鐘才能回顯到螢幕上。這說明我們每按一個鍵telnet用戶端都會立刻把該字元發送給伺服器,然後這個字元經過僞終端主裝置和從裝置之後被Shell程序讀取,同時回顯到僞終端從裝置,回顯的字元再經過僞終端主裝置、telnetd伺服器和網絡發回給telnet用戶端,顯示給使用者看。也許你會覺得吃驚,但真的是這樣:每按一個鍵都要在網絡上走個來回!
2. 程序組
2.1 概念和特性
2.2 程序組相關函數
函數名 | 函數原型 | 說明 | 傳回值 |
---|---|---|---|
getpgrp函數 | | 擷取目前程序的程序組ID | 總是傳回調用者的程序組ID |
getpgid函數 | | 擷取指定程序的程序組ID | 成功:0;失敗:-1,設定errno, |
setpgid函數 | | 改變程序預設所屬的程序組。通常可用來加入一個現有的程序組或建立一個新程序組。 | 成功:0;失敗:-1,設定errno |
int setpgid(pid_t pid, pid_t pgid);
- 将參1對應的程序,加入參2對應的程序組中。
- 注意:
- 如改變子程序為新的組,應fork後,exec前。
- 權級問題。非root程序隻能改變自己建立的子程序,或有權限操作的程序
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == 0){
//子程序
cout << "child PID is : " << getpid() << endl;
cout << "child group ID is : " << getpgid(0) << endl;;//傳回組ID,同下
//cout << "child group ID is : " << getpgrp() << endl;//傳回組ID,
sleep(6);//休眠一下,友善父程序修改子程序使用者組
cout << "-----group id of child is changeed to : " << getpgrp() << endl;
exit(0);
}else if(pid > 0){//父程序
sleep(1);//讓子程序列印其pid和gpid
//子程序自立門戶,把它自己的pid當做使用者組id
setpgid(pid,pid);
sleep(15);
cout << endl;
cout << "parent PID is : " << getpid() << endl;
cout << "parent's parent PID is :" << getppid() << endl;
cout << "parent group PID is : " << getpgid(0) << endl;
sleep(5);
setpgid(getpid(),getppid());//改變父程序的組id為父程序的父程序
cout << "group id of parent is changed to :" << getpgid(0) << endl;
}else{
perror("fork error");
exit(1);
}
return 0;
}
3. 會話
3.1 建立會話
3.2 getsid函數
擷取程序所屬的會話ID
pid_t getsid(pid_t pid);
- 成功:傳回調用程序的會話ID;失敗:-1,設定errno
- pid為0表示察看目前程序session ID
ps ajx指令檢視系統中的程序。
- 參數a表示不僅列目前使用者的程序,也列出所有其他使用者的程序
- 參數x表示不僅列有控制終端的程序,也列出所有無控制終端的程序
- 參數j表示列出與作業控制相關的資訊。
組長程序不能成為新會話首程序,新會話首程序必定會成為組長程序。
3.3 setsid函數
建立一個會話,并以自己的ID設定程序組ID,同時也是新會話的ID。
pid_t setsid(void);
- 成功:傳回調用程序的會話ID;失敗:-1,設定errno
- 調用了setsid函數的程序,既是新的會長,也是新的組長。
fork一個子程序,并使其建立一個新會話。檢視程序組ID、會話ID前後變化
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
pid_t pid = fork();
if(pid == 0){
cout << "child PID is : " << getpid() << endl;
cout << "group PID of child is : " << getpgid(0) << endl;
cout << "session PID of child is : " << getsid(0) << endl;
sleep(10);
setsid();//子程序非組長程序,故可以成為新回話首程序,且成為組長程序,該程序組id為會話程序
cout << "changeed:.............." << endl;
cout << "child PID is : " << getpid() << endl;
cout << "group PID of child is : " << getpgid(0) << endl;
cout << "session PID of child is : " << getsid(0) << endl;
sleep(20);
exit(0);
}else if (pid < 0){
perror("fork error");
exit(1);
}
return 0;
}
4. 守護程序
Daemon(精靈)程序,是Linux中的背景服務程序,通常獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。一般采用以d結尾的名字。
Linux背景的一些系統服務程序,沒有控制終端,不能直接和使用者互動。不受使用者登入、登出的影響,一直在運作着,他們都是守護程序。如:預讀入緩輸出機制的實作;ftp伺服器;nfs伺服器等。
建立守護程序,最關鍵的一步是調用setsid函數建立一個新的Session,并成為Session Leader(因為會話沒有終端)。
建立守護程序模型
-
建立子程序,父程序退出
所有工作在子程序中進行形式上脫離了控制終端
-
在子程序中建立新會話
setsid()函數
使子程序完全獨立出來,脫離控制
-
改變目前目錄為根目錄
chdir()函數
防止占用可解除安裝的檔案系統
也可以換成其它路徑
-
重設檔案權限掩碼
umask()函數
防止繼承的檔案建立屏蔽字拒絕某些權限
增加守護程序靈活性
-
關閉檔案描述符
繼承的打開檔案不會用到,浪費系統資源,無法解除安裝
将0、1、2重定向 /dev/null
dup2()
-
開始執行守護程序核心工作
周期性地執行某種任務或等待處理某些發生的事件
- 守護程序退出處理程式模型
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
using namespace std;
void mydaemond(){
pid_t pid,sid;
//1. fork()一個子程序
pid = fork();
//2. 建立會話
sid = setsid();
//
//3. 改變工作目錄為根目錄
int ret = chdir("/home/du2020/");
if(ret == -1){
perror("chdir error");
exit(1);
}
//4. 設定掩碼
umask(0002);
//5. 重定向檔案描述符,0,1,2指向dev/null空檔案
close(STDIN_FILENO);
open("/dev/null",O_RDWR);
dup2(0,STDOUT_FILENO);
dup2(0,STDERR_FILENO);
}
int main(){
mydaemond();
//6.守護程序
while(1){
;
}
}
.bashrc 可配置bash