问题聚焦:
    除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范。
    工欲善其事,必先利其器,这篇主要来探讨服务器程序的一些主要规范。

概览:
  • 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守护进程通信。
函数定义:
  1. #include <syslog.h>
  2. void syslog ( int priority, const char* message, ... ); // 可变参数
函数说明:
第二个和第三个参数为可变参数,为了结构化输出
priority参数:设施值与日志级别的按位或。设施值的默认值是LOG_USER,可选值如下:
  1. #include <syslog.h>
  2. #define LOG_EMERG 0 /* 系统不可用 */
  3. #define LOG_ALERT 1 /* 报警,需要立即采取动作 */
  4. #define LOG_CRIT 2 /* 非常严重的情况 */
  5. #define LOG_ERR 3 /* 错误 */
  6. #define LOG_WARNING 4 /* 警告 */
  7. #define LOG_NOTICE 5 /* 通知 */
  8. #define LOG_INFO 7 /* 信息 */
  9. #define LOG_DEBUG 8 /* 调试 */
函数:openlog
声明:
  1. #include <syslog.h>
  2. void openlog ( const char* ident, int logopt, int facility );
作用:改变syslog的默认输出方式,进一步结构化日志内容
函数说明:
ident:指定的字符串将被添加到日志消息的日期和时间之后,通常被设置为程序的名字。
logopt:对后续syslog调用的行为进行配置,可取下列值的按位或:
  1. #define LOG_PID 0x01 /* 在日志消息中包含程序PID */
  2. #define LOG_CONS 0x02 /* 如果消息不能记录到日志文件,则打印至终端 */
  3. #define LOG_ODELAY 0x04 /* 延迟打开日志功能直到第一次调用syslog */
  4. #define LOG_NDELAY 0x08 /* 不延迟打开日志功能 */
facility:修改syslog函数中的默认设施值
日志掩码:使日志级别大于日志掩码的日至信息被系统忽略。
函数:
  1. #include <syslog.h>
  2. int setlogmask( int maskpri );
函数说明:
maskpri:指定日志掩码值。
该函数始终会成功,返回调用进程先前的日志掩码值。
例如:
  1. setlogmask(LOG_ERR); //仅仅记录ERR级别的日志消息
  2. setlogmask(LOG_UPTO(LOG_ERR)); //记录ERR以及之前的所有日志的消息[0,3]
关闭日志功能:
  1. #include <syslog.h>
  2. void closelog();

用户信息
用户信息包括:
  • UID:实际用户ID
  • EUID:有效用户ID
  • GID:实际组ID
  • EGID:有效组ID
  1. #include <sys/types.h>
  2. #include <unistd.h>
  3. uid_t getuid(); //获取实际用户ID
  4. uid_t geteuid(); //获取有效用户ID
  5. gid_t getgid(); // 获取实际组ID
  6. gid_t getegid(); // 获取有效组ID
  7. int setuid( uid_t uid ); //设置实际用户ID
  8. int seteuid( uid_t uid ); //设置有效用户ID
  9. int setgid( gid_t gid ); // 设置实际组ID
  10. int setegid( gid_t gid ); // 设置有效组ID
从函数名很容易看出函数的作用,就不解释了。
说明一下UID和EUID的区别:
一个进程拥有两个用户ID:UID和EUID。
EUID存在的目的是方便资源访问,它使得运行程序的用户拥有该资源的有效用户的权限,比如root用户。
实际用户指的是进程的执行者是谁,当用户使用用户名和密码成功登陆一个linux系统后就唯一确定其ID。
有效用户ID指的是进程执行时对文件的访问权限。
测试进程的UID和EUID的区别:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. int main()
  5. {
  6. uid_t uid = getuid();
  7. uid_t euid = geteuid();
  8. printf("userid is %d,effective userid is %d\n",uid,euid);
  9. return 0;
  10. }

  1. @ubuntu:~$ sudo chown root:root test_uid // 修改目标文件的所有者为root
  2. @ubuntu:~$ sudo chmod +s test_uid // 设置目标文件的set-user-id 标志 注意这段很重要
  3. @ubuntu:~$ ./test_uid // 运行程序
  4. userid is 1000,effective userid is 0

从测试程序的输出来看,进程的UID是启动程序的用户的ID,而EUID则是root账户(文件的所有者) 的ID。


进程间关系
进程组
进程中:每个进程都隶属于一个进程组,所以每个进程还有一个进程组ID(PGID)。
函数:
  1. #include <unistd.h>
  2. pid_t getpgid( pid_t pid );
每个进程都有一个首领进程,其PGID和PID相同。进程组将一直存在,直到其中所有进程都退出,或者加入到其他进程中。
设置PGID函数:
  1. #include <unistd.h>
  2. 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。
会话
会话:一些有关联的进程组形成一个会话。
函数:
  1. #include <unistd.h>
  2. pid_t setsid( void );
函数说明:
该函数不能由进程组的首领进程调用,否则将产生一个错误。对于非组首领的进程,调用该函数不仅创建新会话,而且有如下额外效果:
  • 调用进程成为会话的首领,此时该进程是该会话的唯一成员。
  • 新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领。
  • 调用进程将甩开终端(如果有的话)。
会话ID即为首领所在进程组的PGID,获取会话ID(SID)函数:
  1. #include <unistd.h>
  2. pid_t getsid ( pid_t pid );
用ps命令查看进程关系
调用结果如图(PPID为父进程的PID):
三个进程的关系如图:

系统资源限制
Linux系统资源限制可以通过如下一对函数来读取和设置:
  1. #include <sys/resource.h>
  2. int getrlimit( int resource, struct rlimit *rlim );
  3. int setrlimit( int resource, const struct rlimit *rlim );
  4. rlimit结构体的定义如下:
  5. struct rlimit
  6. {
  7. rlim_t rlim_cur; //软限制
  8. rlim_t rlim_max; //硬限制
  9. };
函数说明:
rlim_t 是一个整数类型,它描述资源级别。
rlim_cur:指定资源的软限制。软限制是建议性的、最好不要超越的限制,如果超越的话,系统可能向进程发送信号以终止其运行。
rlim_max:指定资源的硬限制。硬限制是软限制的上限。普通程序可以减小硬限制,而只有以root身份运行的程序才能增加硬限制。
resource:指定资源限制类型。
此外,我们可以使用ulimit命令修改当前shell环境下的资源限制(软限制或硬限制),这种修改将对该shell启动的所有后续程序有效。
部分资源限制类型如下所示:

改变工作目录和根目录
获取当前工作目录和改变进程工作目录的函数分别是:
  1. #include <unistd.h>
  2. char* getcwd( char* buf, size_t size );
  3. int chdir( const char* path );
函数说明:
buf:指向的内存用于存储进程当前工作目录的绝对路径。
size:指定buf的大小,如果当前工作目录的绝对路径的长度超过了size,则getcwd将返回NULL,并设置errno为ERANGE。
path:指定要切换的目标目录。成功时返回0,失败返回-1并设置errno。
改变进程根目录的函数是chroot,其定义如下:
  1. #include <unistd.h>
  2. int chroot( const char* path );
函数说明:
path:指定要切换的目标根目录。
chroot并不改变进程的当前工作目录,所以调用chroot之后,我们仍需要使用chdir("/")来将工作目录切换至新的根目录。
改变进程的根目录之后,程序可能无法访问类似/dev 的文件(和目录),因为这些文件(和目录)并非处于新的根目录之下。不过好在调用chroot之后,进程原先打开的文件描述符依然生效,所以我们可以利用这些早先打开的文件描述符来访问调用chroot之后不能直接访问的文件(和目录),尤其是一些日志文件。
注意:只有超级用户才允许改变根目录,子进程将继承新的根目录。

服务器程序后台化
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. #include <sys/stat.h>
  6. #include <sys/types.h>
  7. #include <fcntl.h>
  8.  
  9. bool daemonize()
  10. {
  11. pid_t pid = fork();
  12. if (pid < 0) {
  13. return false;
  14. }
  15. else if (pid > 0) {
  16. exit(0);
  17. }
  18.  
  19. umask(0);
  20.  
  21. //设置新的会话,设置本进程为进程组的首领
  22. pid_t sid = setsid();
  23. if (sid < 0)
  24. return false;
  25.  
  26. //切换工作目录
  27. if ((chdir("/")) < 0)
  28. return false;
  29.  
  30. printf("2. pid: %ld, parent id: %ld\n", (long)getpid(), (long)getppid());
  31.  
  32. //关闭标准输入,标准输出,标准错误输出
  33. close(STDIN_FILENO);
  34. close(STDOUT_FILENO);
  35. close(STDERR_FILENO);
  36.  
  37. //重定向标准输入,标准输出,标准错误输出到/dev/null
  38. open("/dev/null", O_RDONLY);
  39. open("/dev/null", O_RDWR);
  40. open("/dev/null", O_RDWR);
  41. return true;
  42. }
  43.  
  44. int main(int argc, char **argv)
  45. {
  46. printf("1. pid: %ld, parent id: %ld\n", (long)getpid(), (long)getppid());;
  47. daemonize();
  48. return 0;
  49. }

daemon函数,实现了上述守护进程的功能:

  1. #include <unistd.h>
  2. int daemon(int nochdir, int noclose);

nochdir参数用于指定是否改变工作目录,如果是0,则工作目录将设置为”/“, 否则继续使用当前工作目录。

noclose参数为0时,标准输入,标准输出,标准错误输出都被重定向到/dev/null,否则依然使用原来的设备。

调用成功时返回0,失败时返回-1,并设置errno。

小结:
这章虽然叫做程序规范,其实更多的是介绍一些开发过程,特别是中大型项目中很有用的技巧,可以提高我们的开发和调试效率。
后面将会介绍一些常用的设计框架,这些都是比较基础的知识。后面如果还有时间,希望可以用我们学到的这些知识做一个小项目出来。也算是学以致用了。
参考资料:
《Linux高性能服务器编程》

Linux 高性能服务器编程——Linux服务器程序规范的更多相关文章

  1. Linux高性能server编程——Linux网络基础API及应用

     Linux网络编程基础API 具体介绍了socket地址意义极其API,在介绍数据读写API部分引入一个有关带外数据发送和接收的程序,最后还介绍了其它一些辅助API. socket地址API 主 ...

  2. Linux 高性能服务器编程——Linux网络编程基础API

    问题聚焦:     这节介绍的不仅是网络编程的几个API     更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系.     这节主要介绍三个方面的内容:套接字(so ...

  3. Linux高性能server编程——定时器

    版权声明:本文为博主原创文章.未经博主允许不得转载. https://blog.csdn.net/walkerkalr/article/details/36869913  定时器 服务器程序通常管 ...

  4. Linux高性能server编程——I/O复用

     IO复用 I/O复用使得程序能同一时候监听多个文件描写叙述符.通常网络程序在下列情况下须要使用I/O复用技术: client程序要同一时候处理多个socket client程序要同一时候处理用户 ...

  5. Linux高性能server编程——信号及应用

     信号 信号是由用户.系统或者进程发送给目标进程的信息.以通知目标进程某个状态的改变或系统异常. Linux信号可由例如以下条件产生: 对于前台进程.用户能够通过输入特殊的终端字符来给它发送信号. ...

  6. Linux高性能server编程——多线程编程(下)

    多线程编程 条件变量 假设说相互排斥锁是用于同步线程对共享数据的訪问的话.那么条件变量则是用于线程之间同步共享数据的值. 条件变量提供了一种线程间的通信机制:当某个共享数据达到某个值得时候,唤醒等待这 ...

  7. arp学习笔记(linux高性能服务编程)

    先看看arp的定义吧 现在linux运行这条命令 tcpdump -i eth0:1 -ent '(dst 192.168.5.190 and src 192.168.5.109)or( dst 19 ...

  8. Linux高性能server编程——高级I/O函数

     高级I/O函数 pipe函数 pipe函数用于创建一个管道,实现进程间的通信. #include <unistd.h> int pipe(int pipefd[2]); 通过pipe ...

  9. Linux 高性能server编程——高级I/O函数

    重定向dup和dup2函数 #include <unistd.h> int dup(int file_descriptor); int dup2(int file_descriptor_o ...

随机推荐

  1. Java四种线程池的学习与总结

    在Java开发中,有时遇到多线程的开发时,直接使用Thread操作,对程序的性能和维护上都是一个问题,使用Java提供的线程池来操作可以很好的解决问题. 一.new Thread的弊端 执行一个异步任 ...

  2. Markdown 编辑器使用指南

    Markdown 编辑器使用指南 1.快捷键 加粗: Ctrl/Cmd + B 标题: Ctrl/Cmd + H 插入链接: Ctrl/Cmd + K 插入代码: Ctrl/Cmd + Shift + ...

  3. Java IO(四)

    在文件操作流中,输入输出的目标都是文件,但是有时候,我们并不需要写入文件,只是需要中转一下而已,这样就会显得很麻烦,所以我们就可以使用内存操作流.在内存操作流中,输入输出目标都是内存. 内存输出流:B ...

  4. [JSOI 2011]分特产

    Description JYY 带队参加了若干场ACM/ICPC 比赛,带回了许多土特产,要分给实验室的同学们. JYY 想知道,把这些特产分给N 个同学,一共有多少种不同的分法?当然,JYY 不希望 ...

  5. [WC 2011]Xor

    Description Input 第一行包含两个整数N和 M, 表示该无向图中点的数目与边的数目. 接下来M 行描述 M 条边,每行三个整数Si,Ti ,Di,表示 Si 与Ti之间存在 一条权值为 ...

  6. hdu 5887 搜索+剪枝

    Herbs Gathering Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)T ...

  7. NOIP2014-5-10模拟赛

    Problem 1 机器人(robot.cpp/c/pas) [题目描述] 早苗入手了最新的Gundam模型.最新款自然有着与以往不同的功能,那就是它能够自动行走,厉害吧. 早苗的新模型可以按照输入的 ...

  8. ●BZOJ 2839 集合计数

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2839 题解: 容斥原理 真的是神题!!! 定义 f[k] 表示交集大小至少为 k时的方案数怎 ...

  9. solr6.6初探之分词篇

    关于solr6.6搭建与配置可以参考 solr6.6初探之配置篇 在这里我们探讨一下分词的配置 一.关于分词 1.分词是指将一个中文词语拆成若干个词,提供搜索引擎进行查找,比如说:北京大学 是一个词那 ...

  10. mysql免安装版下载及配置教程

    第一步:下载 下载地址:http://dev.mysql.com/downloads/mysql/ 滚动到下方就能看到了,根据自己的需求下载: 我的电脑为64为的所以下载的为 Windows (x86 ...