第七章 Linux服务器程序规范
服务器程序除了需要网络通信外,还应该考虑很多其他的细节,而这些细节很多很杂,但又基本是模板式的。1)服务器程序基本都是以后台形式运行的,没有控制终端,不能接受用户输入,其父进程通常是init。2)服务器程序有一套日志系统。3)服务器程序以某个专门的非root身份运行。4)服务器通常是可配置的。5)服务器进程启动时通常会生成一个PID文件以记录后台进程的PID。6)服务器程序同城需要考虑系统资源和限制。
服务器一般使用syslog函数与rsyslogd守护进程通信,其参数需要指示日志级别和格式化输出,openlog函数可以改变默认输出方式,setlogmask函数可以设置日志掩码,如果日志级别大于掩码的日志信息被忽略。
一个进程有两个用户ID:UID和EUID,UID一般是进程创建者的ID,而EUID则是进程对文件和资源访问的权限,举个例子:/etc/passwd文件需要root权限才能访问,但是当普通用户想要访问时需要在命令前加上sudo,其原理就是sudo改变了EUID,使其能够访问passwd文件。
我们使用一个能更改当前进程的运行权限的例子来说明一下,下面的程序可以将以root身份启动的进程变成以普通用户身份运行。
1 /************************************************************************* 2 > File Name: 7-2.cpp 3 > Author: Torrance_ZHANG 4 > Mail: [email protected] 5 > Created Time: Fri 02 Feb 2018 07:17:19 PM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 static bool switch_to_user(uid_t user_id, gid_t gp_id) { 12 //确保目标用户不是root 13 if((user_id == 0) && (gp_id == 0)) return false; 14 gid_t gid = getgid(); 15 uid_t uid = getuid(); 16 //确保当前用户是合法用户,或者是root或者已经是目标用户 17 if(((gid != 0) || (uid != 0)) && ((gid != gp_id) || (uid != user_id))) return false; 18 //如果不是root用户则已经是目标用户 19 if(uid != 0) return true; 20 //设置为目标用户 21 if((setgid(gp_id) < 0) || (setuid(user_id) < 0)) return false; 22 return true; 23 } 24 25 int main() { 26 uid_t user_id; 27 gid_t gp_id; 28 user_id = getuid(); 29 gp_id = getgid(); 30 printf("uid = %d, gid = %d\\n", user_id, gp_id); 31 if(switch_to_user(1000, 1000) == false) { 32 perror("switch_to_user"); 33 return 1; 34 } 35 user_id = getuid(); 36 gp_id = getgid(); 37 printf("uid = %d, gid = %d\\n", user_id, gp_id); 38 }
root用户的uid和gid都是0,我们成功将其改为了非0的数。
可以说进程的uid和gid都是其所属用户的相关信息,接下来我们来看一下进程本身都有什么属性信息及进程之间都有什么关系。在Linux下每个进程都隶属于一个进程组,所以进程除了pid信息外还有pgid,而每个进程组都有一个首领进程,其pgid和pid相同,值得一提的是,一个进程只能设置自己和其子进程的pgid,并且当子进程进行exec函数族调用后就不能在父进程中改变其pgid。而多个进程组可以形成一个会话,但是创建会话的进程不能是某个进程组的首领进程,否则将产生错误。对于其余进程创建会话,则有如下效果:1)调用进程成为会话的首领,此时该进程是新会话的唯一成员。2)新建一个进程组,其pgid就是调用进程的pid,调用进程成为该组首领。3)调用进程将甩开终端。
最后我看一下如何将进程以守护进程的形式运行,代码如下:
1 /************************************************************************* 2 > File Name: 7-3.cpp 3 > Author: Torrance_ZHANG 4 > Mail: [email protected] 5 > Created Time: Fri 02 Feb 2018 07:59:21 PM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 bool daemonize() { 12 //创建新进程并将父进程退出可以使子进程后台运行 13 pid_t pid = fork(); 14 if(pid < 0) return false; 15 else if(pid > 0) exit(0); 16 17 //清空文件掩码,这样新建的文件权限为mode&0777 18 umask(0); 19 20 //创建新会话并设置本进程为进程组首领 21 pid_t sid = setsid(); 22 if(sid < 0) return false; 23 24 //改变工作目录 25 if((chdir("/")) < 0) return false; 26 27 //关闭标准设备 28 close(STDIN_FILENO); 29 close(STDOUT_FILENO); 30 close(STDERR_FILENO); 31 32 //将其重定向 33 open("/dev/null", O_RDONLY); 34 open("/dev/null", O_RDWR); 35 open("/dev/null", O_RDWR); 36 } 37 38 int main() { 39 daemonize(); 40 while(1) sleep(1); 41 }
我们发现程序确实以守护进程形式运行,有意思的是,第一次运行时发现其父进程不是我们熟知的托管孤儿进程的init,而是另外一个/sbin/upstart --user,通过查询资料发现这个进程是Ubuntu图形化界面的一个守护进程,完成了init进程的一部分功能,当切换到命令行界面执行程序后,父进程就变成了init。此外,Linux系统还提供了一个daemon函数完成这个功能。