Linux 高性能服務器編程——Linux服務器程序規範

問題聚焦:
    除了網絡通訊外,服務器程序一般還必須考慮許多其餘細節問題,這些細節問題涉及面逛且零碎,並且基本上是模板式的,因此稱之爲服務器程序規範。
    工欲善其事,必先利其器,這篇主要來探討服務器程序的一些主要規範。



概覽:
  • Linux服務器程序通常之後臺程序的形式運行,後臺進程又稱爲守護進程。
  • Linux服務器程序通常以某個專門的非root身份運行。
  • Linux服務器程序一般是可配置的,命令行或者配置文件的形式。
  • Linux服務器程序一般會在啓動的時候生成一個PID文件,以記錄該後臺進程的PID。
  • Linux服務器程序一般須要考慮系統資源和限制。



日誌

守護進程: rsyslogd
功能:接收用戶進程輸出的日誌,又能接收內核日誌。

用戶日誌函數: syslog
功能:生成系統日誌,輸出到一個UNIX本地域socket類型(AF_UNIX)的文件/dev/log中。rsyslogd則監聽該文件以獲取用戶進程的輸出。

內核日誌函數: printk
功能:打印至內核的環狀緩存中,環狀緩存的內容直接映射到/proc/kmsg文件中。rsyslogd則經過讀取該文件得到內核日誌。

處理流程以下圖所示:


rsyslogd守護進程在接收到用戶進程或內核輸入的日誌後,會把它們輸出至某些特定的日誌文件。默認狀況下,調試信息則保存至/var/log/debug 文件,普通訊息保存至/var/log/messages 文件,內核消息則保存至/var/log/kern.log 文件。不過,日誌信息具體如何分發,能夠在rsyslogd的配置文件中設置。rsyslogd的主配置文件是/etc/rsyslog.conf,其中主要能夠設置的項包括:內核日誌及其監聽端口(默認是514,見/etc/services 文件),是否接收TCP日誌及其監聽端口,日誌文件的權限,包含哪些子配置文件(好比/etc/rsyslog.d/*.conf)。rsyslogd的子配置文件則指定各種日誌的目標存儲文件。



syslog函數

功能:應用程序使用syslog函數與rsyslogd守護進程通訊。
函數定義:
#include <syslog.h>
void syslog ( int priority, const char* message, ... );    // 可變參數
函數說明:
第二個和第三個參數爲可變參數,爲告終構化輸出
priority參數:設施值與日誌級別的按位或。 設施值的默認值是LOG_USER,可選值以下:
#include <syslog.h>
#define LOG_EMERG            0      /* 系統不可用 */
#define LOG_ALERT              1      /* 報警,須要當即採起動做 */
#define LOG_CRIT                 2      /* 很是嚴重的狀況 */
#define LOG_ERR                  3     /* 錯誤 */
#define LOG_WARNING       4     /* 警告 */
#define LOG_NOTICE           5      /* 通知 */
#define LOG_INFO               7      /* 信息 */
#define LOG_DEBUG           8      /* 調試 */

函數:openlog
聲明:
#include <syslog.h>
void openlog ( const char* ident, int logopt, int facility );
做用:改變syslog的默認輸出方式,進一步結構化日誌內容
函數說明:
ident:指定的字符串將被添加到日誌消息的日期和時間以後,一般被設置爲程序的名字。
logopt:對後續syslog調用的行爲進行配置,可取下列值的按位或:
#define    LOG_PID        0x01            /* 在日誌消息中包含程序PID */
#define    LOG_CONS    0x02            /* 若是消息不能記錄到日誌文件,則打印至終端 */
#define    LOG_ODELAY    0x04        /* 延遲打開日誌功能直到第一次調用syslog */
#define    LOG_NDELAY    0x08        /* 不延遲打開日誌功能 */
facility:修改syslog函數中的默認設施值

日誌掩碼:使日誌級別大於日誌掩碼的日至信息被系統忽略。
函數:
#include <syslog.h>
int setlogmask( int maskpri );
函數說明:
maskpri:指定日誌掩碼值。
該函數始終會成功,返回調用進程先前的日誌掩碼值。

例如:
setlogmask(LOG_ERR); //僅僅記錄ERR級別的日誌消息
setlogmask(LOG_UPTO(LOG_ERR)); //記錄ERR以及以前的全部日誌的消息[0,3]

關閉日誌功能:
#include <syslog.h>
void closelog();


用戶信息

用戶信息包括:
  • UID:實際用戶ID
  • EUID:有效用戶ID
  • GID:實際組ID
  • EGID:有效組ID
#include <sys/types.h>
#include <unistd.h>
uid_t getuid();   //獲取實際用戶ID                 
uid_t geteuid();  //獲取有效用戶ID
gid_t getgid();  // 獲取實際組ID
gid_t getegid();  // 獲取有效組ID
int setuid( uid_t uid );  //設置實際用戶ID
int seteuid( uid_t uid );  //設置有效用戶ID
int setgid( gid_t gid );  // 設置實際組ID
int setegid( gid_t gid );  // 設置有效組ID
從函數名很容易看出函數的做用,就不解釋了。

說明一下UID和EUID的區別:
一個進程擁有兩個用戶ID:UID和EUID。
EUID存在的目的是方便資源訪問,它使得運行程序的用戶擁有該資源的有效用戶的權限,好比root用戶。

實際用戶指的是進程的執行者是誰,當用戶使用用戶名和密碼成功登錄一個linux系統後就惟一肯定其ID。
有效用戶ID指的是進程執行時對文件的訪問權限。
測試進程的UID和EUID的區別:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
    uid_t uid = getuid();
    uid_t euid = geteuid();
    printf("userid is %d,effective userid is %d\n",uid,euid);
    return 0;
}

@ubuntu:~$ sudo chown root:root test_uid  // 修改目標文件的全部者爲root
@ubuntu:~$ sudo chmod +s test_uid    // 設置目標文件的set-user-id  標誌     注意這段很重要
@ubuntu:~$ ./test_uid  // 運行程序
userid is 1000,effective userid is 0
從測試程序的輸出來看,進程的UID是啓動程序的用戶的ID,而EUID則是root帳戶(文件的全部者) 的ID。



進程間關係

進程組
進程中:每一個進程都隸屬於一個進程組,因此每一個進程還有一個進程組ID(PGID)。
函數:
#include <unistd.h>
pid_t getpgid( pid_t pid );
每一個進程都有一個首領進程,其PGID和PID相同。進程組將一直存在,直到其中全部進程都退出,或者加入到其餘進程中。

設置PGID函數:
#include <unistd.h>
int setpgid( pid_t pid, pid_t pgid );
做用:將PID爲pid的進程的PGID設置爲pgid。若是pid和pgid相同,則由pid指定的進程將被設置爲進程組首領;若是pid爲0,則表示設置當前進程的PGID爲pgid;若是pgid爲0,則使用pid做爲目標PGID。setpgid函數成功時返回0,失敗則返回-1並設置errno。

一個進程只能設置本身或者其子進程的PGID,而且,當子進程調用exec系列函數後,咱們也不能再在父進程中對它設置PGID。


會話
會話:一些有關聯的進程組造成一個會話。
函數:
#include <unistd.h>
pid_t setsid( void );
函數說明:
該函數不能由進程組的首領進程調用,不然將產生一個錯誤。對於非組首領的進程,調用該函數不只建立新會話,並且有以下額外效果:
  • 調用進程成爲會話的首領,此時該進程是該會話的惟一成員。
  • 新建一個進程組,其PGID就是調用進程的PID,調用進程成爲該組的首領。
  • 調用進程將甩開終端(若是有的話)。
會話ID即爲首領所在進程組的PGID,獲取會話ID(SID)函數:
#include <unistd.h>
pid_t getsid ( pid_t pid );

用ps命令查看進程關係
調用結果如圖(PPID爲父進程的PID):

三個進程的關係如圖:




系統資源限制

Linux系統資源限制能夠經過以下一對函數來讀取和設置:
#include <sys/resource.h>
int getrlimit( int resource, struct rlimit *rlim );
int setrlimit( int resource, const struct rlimit *rlim );
rlimit結構體的定義以下:
struct rlimit
{
    rlim_t rlim_cur;  //軟限制
    rlim_t rlim_max;  //硬限制
};
函數說明:
rlim_t 是一個整數類型,它描述資源級別。
rlim_cur:指定資源的軟限制。軟限制是建議性的、最好不要超越的限制,若是超越的話,系統可能向進程發送信號以終止其運行。
rlim_max:指定資源的硬限制。 硬限制是軟限制的上限。普通程序能夠減少硬限制,而只有以root身份運行的程序才能增長硬限制。
resource:指定資源限制類型。

此外,咱們可使用ulimit命令修改當前shell環境下的資源限制(軟限制或硬限制),這種修改將對該shell啓動的全部後續程序有效。

部分資源限制類型以下所示:




改變工做目錄和根目錄

獲取當前工做目錄和改變進程工做目錄的函數分別是:
#include <unistd.h>
char* getcwd( char* buf, size_t size );
int chdir( const char* path );
函數說明:
buf:指向的內存用於存儲進程當前工做目錄的絕對路徑。
size:指定buf的大小,若是當前工做目錄的絕對路徑的長度超過了size,則getcwd將返回NULL,並設置errno爲ERANGE。
path:指定要切換的目標目錄。成功時返回0,失敗返回-1並設置errno。

改變進程根目錄的函數是chroot,其定義以下:
#include <unistd.h>
int chroot( const char* path );
函數說明:
path:指定要切換的目標根目錄。
chroot並不改變進程的當前工做目錄,因此調用chroot以後,咱們仍須要使用chdir("/")來將工做目錄切換至新的根目錄。
改變進程的根目錄以後,程序可能沒法訪問相似/dev 的文件(和目錄),由於這些文件(和目錄)並不是處於新的根目錄之下。不過好在調用chroot以後,進程原先打開的文件描述符依然生效,因此咱們能夠利用這些早先打開的文件描述符來訪問調用chroot以後不能直接訪問的文件(和目錄),尤爲是一些日誌文件。
注意:只有超級用戶才容許改變根目錄,子進程將繼承新的根目錄。



服務器程序後臺化

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>

bool daemonize()
{
	pid_t pid = fork();
	if (pid < 0) {
		return false;
	}
	else if (pid > 0) {
		exit(0);
	}
	
	umask(0);

        //設置新的會話,設置本進程爲進程組的首領
	pid_t sid = setsid();
	if (sid < 0)
		return false;
	
        //切換工做目錄	
	if ((chdir("/")) < 0)
		return false;
	
	printf("2. pid: %ld, parent id: %ld\n", (long)getpid(), (long)getppid());
	
        //關閉標準輸入,標準輸出,標準錯誤輸出
	close(STDIN_FILENO);
	close(STDOUT_FILENO);
	close(STDERR_FILENO);

        //重定向標準輸入,標準輸出,標準錯誤輸出到/dev/null
	open("/dev/null", O_RDONLY);
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);
	return true;
}

int main(int argc, char **argv)
{
	printf("1. pid: %ld, parent id: %ld\n", (long)getpid(), (long)getppid());;
	daemonize();	
	return 0;
}

daemon函數,實現了上述守護進程的功能:
#include <unistd.h>
int daemon(int nochdir, int noclose);
nochdir參數用於指定是否改變工做目錄,若是是0,則工做目錄將設置爲」/「, 不然繼續使用當前工做目錄。

noclose參數爲0時,標準輸入,標準輸出,標準錯誤輸出都被重定向到/dev/null,不然依然使用原來的設備。linux

調用成功時返回0,失敗時返回-1,並設置errno。shell


小結:
這章雖然叫作程序規範,其實更多的是介紹一些開發過程,特別是中大型項目中頗有用的技巧,能夠提升咱們的開發和調試效率。
後面將會介紹一些經常使用的設計框架,這些都是比較基礎的知識。後面若是還有時間,但願能夠用咱們學到的這些知識作一個小項目出來。也算是學以至用了。

參考資料:
《Linux高性能服務器編程》
相關文章
相關標籤/搜索