做网站和做网页的区别,wordpress站内搜索,wordpress怎么破解主题,安徽网站推广系统Linux高性能服务器编程
本文是读书笔记#xff0c;如有侵权#xff0c;请联系删除。
参考
Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes
豆瓣: Linux高性能服务器编程 文章目录 Linux高性能服务器编程第07章 Linux服务器程序规范7.1日志7.2用…Linux高性能服务器编程
本文是读书笔记如有侵权请联系删除。
参考
Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes
豆瓣: Linux高性能服务器编程 文章目录 Linux高性能服务器编程第07章 Linux服务器程序规范7.1日志7.2用户信息7.3进程间关系7.4系统资源限制7.5改变工作目录和根目录7.6服务器程序后台化后记 第07章 Linux服务器程序规范
除了网络通信外服务器程序通常还必须考虑许多其他细节问题。这些细节问题涉及面 广且零碎而且基本上是模板式的所以我们称之为服务器程序规范。比如
Linux服务器程序一般以后台进程形式运行。后台进程又称守护进程(daemon)。它 没有控制终端因而也不会意外接收到用户输入。守护进程的父进程通常是init进程PID为1的进程。
Linux服务器程序通常有一套日志系统它至少能输出日志到文件有的高级服务器还能输出日志到专门的UDP服务器。大部分后台进程都在/var/log目录下拥有自己的 日志目录。
Linux服务器程序一般以某个专门的非root身份运行。比如mysqld、httpd、syslogd 等后台进程分别拥有自己的运行账户mysql、apache和syslog。Linux服务器程序通常是可配置的。服务器程序通常能处理很多命令行选项如果一 次运行的选项太多则可以用配置文件来管理。绝大多数服务器程序都有配置文件 并存放在/etc目录下。比如第4章讨论的squid服务器的配置文件是/etc/squid3/squid.conf。
Linux服务器进程通常会在启动的时候生成一个PID文件并存入/var/run目录中以 记录该后台进程的 PID。比如syslogd 的PID 文件是/var/run/syslogd.pid。
Linux服务器程序通常需要考虑系统资源和限制以预测自身能承受多大负荷比如进程可用文件描述符总数和内存总量等。
在开始系统地学习网络编程之前我们将用一章的篇幅来探讨服务器程序的一些主要的规范。
7.1日志
7.1.1Linux系统日志
工欲善其事必先利其器。服务器的调试和维护都需要一个专业的日志系统。Linux提供一个守护进程来处理系统日志—syslogd不过现在的Linux系统上使用的都是它的升级 版——rsyslogd。rsyslogd守护进程既能接收用户进程输出的日志又能接收内核日志。用户进程是通过 调用syslog函数生成系统日志的。该函数将日志输出到一个UNIX本地域socket类型AF_ UNIX的文件/dev/log中rsyslogd则监听该文件以获取用户进程的输出。内核日志在老的系统上是通过另外一个守护进程rklogd来管理的rsyslogd利用额外的模块实现了相同的功能。内核日志由printk等函数打印至内核的环状缓存ring buffer)中。环状缓存的内容直接 映射到/proc/kmsg文件中。rsyslogd则通过读取该文件获得内核日志。
rsyslogd守护进程在接收到用户进程或内核输入的日志后会把它们输出至某些特定的 日志文件。默认情况下调试信息会保存至/var/log/debug 文件普通信息保存至/var/log/ messages文件内核消息则保存至/var/log/kern.log文件。不过日志信息具体如何分发可 以在rsyslogd的配置文件中设置。rsyslogd的主配置文件是/etc/rsyslog.conf,其中主要可以设置的项包括内核日志输入路径是否接收UDP日志及其监听端口(默认是514见/etc/services文件)是否接收TCP日志及其监听端口日志文件的权限包含哪些子配置文件(比如/etc/rsyslog.d/*.conf)。rsyslogd的子配置文件则指定各类日志的目标存储文件。图7-1总结了Linux的系统日志体系。 7.1.2 syslog 函数
应用程序使用syslog函数与rsyslogd守护进程通信。syslog 函数的定义如下
#include syslog.h
void syslog( int priority, const char* message, …. );该函数采用可变参数第二个参数message和第三个参数…来结构化输出。priority参数是所谓的设施值与日志级别的按位或。设施值的默认值是LOG_USER,我们下面的讨论也 只限于这一种设施值。日志级别有如下几个 #include syslog.hvoid syslog( int priority, const char* message, ... );解释 priority 指定消息的优先级它包括设施facility和级别level两个部分。可以通过 LOG_FACMASK 和 LOG_PRIMASK 进行掩码操作获取或设置这两个部分。 message 要记录的消息的格式字符串类似于 printf 中的格式化字符串。
示例
#include syslog.hint main() {openlog(my_program, LOG_PID | LOG_CONS, LOG_USER);syslog(LOG_INFO, This is an informational message.);syslog(LOG_ERR, An error occurred: %s, File not found);closelog();return 0;
}上述示例中程序首先调用 openlog 打开系统日志指定了程序标识符为 “my_program”同时指定了一些选项LOG_PID 表示在每条日志消息中包含进程IDLOG_CONS 表示将日志消息发送到控制台。然后使用 syslog 记录两条日志消息一条是信息性消息 (LOG_INFO)另一条是错误消息 (LOG_ERR)。最后调用 closelog 关闭系统日志。
下面这个函数可以改变syslog的默认输出方式进一步结构化日志内容
#include syslog.h
void openlog( const char* ident, int logopt, int facility );ident参数指定的字符串将被添加到日志消息的日期和时间之后它通常被设置为程序的名字。logopt参数对后续syslog调用的行为进行配置它可取下列值的按位或 facility参数可用来修改syslog函数中的默认设施值。
此外日志的过滤也很重要。程序在开发阶段可能需要输出很多调试信息而发布之后 我们又需要将这些调试信息关闭。解决这个问题的方法并不是在程序发布之后删除调试代码 因为日后可能还需要用到)而是简单地设置日志掩码使日志级别大于日志掩码的日志信 息被系统忽略。下面这个函数用于设置syslog的日志掩码
#include syslog.h
int setlogmask( int maskpri );maskpri参数指定日志掩码值。该函数始终会成功它返回调用进程先前的日志掩码值。 最后不要忘了使用如下函数关闭日志功能
#include syslog.h
void closelog();7.2用户信息
7.2.1UID、EUID、GID和EGID
用户信息对于服务器程序的安全性来说是很重要的比如大部分服务器就必须以root身 份启动但不能以root身份运行。下面这一组函数可以获取和设置当前进程的真实用户IDUID、有效用户IDEUID、真实组IDGID和有效组IDEGID 需要指出的是一个进程拥有两个用户IDUID和EUID。EUID存在的目的是方便资源访问它使得运行程序的用户拥有该程序的有效用户的权限。比如su程序任何用户都 可以使用它来修改自己的账户信息但修改账户时su程序不得不访问/etc/passwd文件而 访问该文件是需要root权限的。那么以普通用户身份启动的su程序如何能访问/etc/passwd 文件呢窍门就在EUID。用ls命令可以查看到su程序的所有者是root并且它被设置了 set-user-id标志。这个标志表示任何普通用户运行su程序时其有效用户就是该程序的所有者root。那么根据有效用户的含义任何运行su程序的普通用户都能够访问/etc/passwd 文件。有效用户为root的进程称为特权进程(privileged processes)。EGID的含义与EUID类 似给运行目标程序的组用户提供有效组的权限。
#include unistd.h
#include stdio.hint main()
{uid_t uid getuid();uid_t euid geteuid();printf( userid is %d, effective userid is: %d\n, uid, euid );return 0;
}
7.2.2切换用户
下面的代码清单7-2展示了如何将以root身份启动的进程切换为以一个普通用户身份运行。
/*** brief 切换用户和用户组* * param user_id 目标用户ID* param gp_id 目标用户组ID* return 切换是否成功*/
static bool switch_to_user(uid_t user_id, gid_t gp_id)
{// 如果目标用户ID和目标用户组ID都为0表示无需切换if ((user_id 0) (gp_id 0)){return false;}// 获取当前进程的实际用户组ID和实际用户IDgid_t gid getgid();uid_t uid getuid();// 如果当前用户ID不为0表示已经是非root用户无需切换if (((gid ! 0) || (uid ! 0)) ((gid ! gp_id) || (uid ! user_id))){return false;}// 如果当前用户ID不为0表示已经是非root用户无需切换if (uid ! 0){return true;}// 尝试切换用户组和用户IDif ((setgid(gp_id) 0) || (setuid(user_id) 0)){return false;}return true;
}该函数的作用是切换进程的用户ID和用户组ID。函数会检查当前用户ID和用户组ID如果已经是非root用户或者目标用户ID和目标用户组ID为0表示无需切换返回 false。如果当前用户ID为0root用户则尝试使用 setgid 和 setuid 切换到目标用户组和用户ID。切换成功返回 true否则返回 false。
7.3进程间关系
7.3.1进程组
Linux下每个进程都隶属于一个进程组因此它们除了PID信息外还有进程组IDPGID)。我们可以用如下函数来获取指定进程的PGID
#include unistd.h
pid_t getpgid( pid_t pid );该函数成功时返回进程pid所属进程组的PGID失败则返回-1并设置errno。每个进程组都有一个首领进程其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。
7.3.2会话
一些有关联的进程组将形成一个会话session)。下面的函数用于创建一个会话
#include unistd.h
pid_t setsid( void );该函数不能由进程组的首领进程调用否则将产生一个错误。对于非组首领的进程调用该函数不仅创建新会话而且有如下额外效果
调用进程成为会话的首领此时该进程是新会话的唯一成员。
新建一个进程组其PGID就是调用进程的PID调用进程成为该组的首领。
调用进程将甩开终端如果有的话。
该函数成功时返回新的进程组的PGID失败则返回-1并设置errno。
Linux进程并未提供所谓会话ID(SID)的概念但Linux系统认为它等于会话首领所在的进程组的PGID并提供了如下函数来读取SID
#include unistd.h
pid_t getsid( pid_t pid );7.3.3用ps命令查看进程关系
执行ps命令可查看进程、进程组和会话之间的关系 我们是在 bash shell下执行 ps和less命令的所以ps和less命令的父进程是bash命令 这可以从PPID父进程PID一列看出。这3条命令创建了1个会话SID是1943和2 个进程组PGID分别是1943和2298。bash命令的PID、PGID和SID都相同很明显它既是会话的首领也是组1943的首领。ps命令则是组2298的首领因为其PID也是2298。图7-2描述了此三者的关系。 7.4系统资源限制
Linux上运行的程序都会受到资源限制的影响比如物理设备限制(CPU数量、内存数量等)、系统策略限制CPU时间等)以及具体实现的限制比如文件名的最大长度。Linux系统资源限制可以通过如下一对函数来读取和设置
#include sys/resource.h
int getrlimit( int resource, struct rlimitlim ):
int setrlimit( int resource, const struct rlimit *rlim ); rlim参数是rlimit结构体类型的指针rlimit结构体的定义如下
struct rlimit
{rlim_t rlim_cur; rlim_t rlim max;
};rlim_t是一个整数类型它描述资源级别。rlim_cur成员指定资源的软限制rlim_max 成员指定资源的硬限制。软限制是一个建议性的、最好不要超越的限制如果超越的话系 统可能向进程发送信号以终止其运行。例如当进程CPU时间超过其软限制时系统将向进程发送SIGXCPU信号当文件尺寸超过其软限制时系统将向进程发送SIGXFSZ信号 见第10章。硬限制一般是软限制的上限。普通程序可以减小硬限制而只有以root身份 运行的程序才能增加硬限制。此外我们可以使用ulimit命令修改当前shell环境下的资源限 制(软限制或/和硬限制这种修改将对该shell启动的所有后续程序有效。我们也可以通 过修改配置文件来改变系统软限制和硬限制而且这种修改是永久的详情见第16章。
resource参数指定资源限制类型。表7-1列举了部分比较重要的资源限制类型。 7.5改变工作目录和根目录
有些服务器程序还需要改变工作目录和根目录比如我们第4章讨论的 Web 服务器。一般来说Web服务器的逻辑根目录并非文件系统的根目录“/”而是站点的根目录对于Linux的Web 服务来说该目录一般是/var/www/)。
获取进程当前工作目录和改变进程工作目录的函数分别是
#include unistd.h
char* getcwd( char* buf, size_t size );
int chdir( const char* path );buf参数指向的内存用于存储进程当前工作目录的绝对路径名其大小由size参数指定。 如果当前工作目录的绝对路径的长度(再加上一个空结束字符“\0”)超过了size则getcwd 将返回NULL并设置errno为ERANGE。如果buf为NULL并且size非0,则getcwd可能 在内部使用malloc动态分配内存并将进程的当前工作目录存储在其中。如果是这种情况 则我们必须自己来释放getcwd在内部创建的这块内存。getcwd函数成功时返回一个指向目 标存储区buf指向的缓存区或是getcwd在内部动态创建的缓存区的指针失败则返回 NULL并设置errno。
chdir 函数的path参数指定要切换到的目标目录。它成功时返回0失败时返回-1并设 置errno。改变进程根目录的函数是chroot其定义如下
#include unistd.h
int chroot( const char* path );path参数指定要切换到的目标根目录。它成功时返回0失败时返回-1并设置 errno。 chroot并不改变进程的当前工作目录所以调用chroot之后我们仍然需要使用chdir(“/” 来将工作目录切换至新的根目录。改变进程的根目录之后程序可能无法访问类似/dev的文 件和目录)因为这些文件和目录并非处于新的根目录之下。不过好在调用chroot之 后进程原先打开的文件描述符依然生效所以我们可以利用这些早先打开的文件描述符来 访问调用chroot之后不能直接访问的文件(和目录尤其是一些日志文件。此外只有特 权进程才能改变根目录。
7.6服务器程序后台化
最后我们讨论如何在代码中让一个进程以守护进程的方式运行。守护进程的编写遵循 一定的步骤[2]下面我们通过一个具体实现来探讨如代码清单7-3所示。
7-3daemonize.cpp
/*** brief 创建守护进程* * return 创建是否成功*/
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;}// 关闭标准输入、标准输出、标准错误描述符close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 重定向标准输入、标准输出、标准错误到 /dev/nullopen(/dev/null, O_RDONLY);open(/dev/null, O_RDWR);open(/dev/null, O_RDWR);return true;
}该函数的作用是创建守护进程。首先通过 fork 创建子进程父进程退出使子进程成为孤儿进程。然后调用 setsid 创建新会话并使当前进程成为新会话的首进程和组长进程。接着切换工作目录到根目录关闭标准输入、标准输出、标准错误描述符最后将它们重定向到 /dev/null。函数返回创建是否成功。
实际上Linux提供了完成同样功能的库函数
#include unistd.h
int daemon( int nochdir, int noclose );其中nochdir参数用于指定是否改变工作目录如果给它传递0则工作目录将被设置 为“/”根目录否则继续使用当前工作目录。noclose参数为0时标准输入、标准输出 和标准错误输出都被重定向到/dev/null文件否则依然使用原来的设备。该函数成功时返回 0失败则返回-1并设置errno。
对int daemon( int nochdir, int noclose );使用举例
#include unistd.hint main()
{int nochdir 0; // 是否切换工作目录0表示切换非0表示不切换int noclose 0; // 是否关闭标准输入、标准输出、标准错误0表示关闭非0表示不关闭int result daemon(nochdir, noclose);if (result 0){// 守护进程的代码逻辑while (1){// 守护进程的主体工作// ...}}else{// 创建守护进程失败的处理逻辑// ...}return 0;
}daemon 函数是一种创建守护进程的方法该函数会调用 fork 创建子进程并使父进程退出从而让子进程成为孤儿进程。然后setsid 函数将子进程创建为新的会话组长和进程组长与当前终端脱离关系。最后根据 nochdir 和 noclose 参数判断是否切换工作目录和关闭标准输入、标准输出、标准错误。
在代码示例中通过设定 nochdir 和 noclose 参数可以控制是否切换工作目录和关闭标准输入、标准输出、标准错误。函数返回值为0表示创建守护进程成功-1表示失败。
后记
截至2024年1月20日18点38分阅读《Linux高性能服务器编程》第七章记录一下学习过程。