Linux服务器程序规范

除了网络通信外,服务器程序还必须考虑许多其他细节问题,零碎,但基本上时模板式的。 ———引

  • Linux服务器程序一般以后台形式运行。后台程序又称守护进程。它没有控制终端,因而也不会意外接受用户输入。守护进程的父进程一般是init进程(pid=1)。
  • Linux服务器程序通常有一套日志系统,它至少能输出日志到文件,有的高级服务器可以输出日志到专门的UDP服务器。大部分后台进程都在/var/log下有自己的日志目录。
  • Linux服务器程序一般以某个专门的非root身份运行。mysqld, httpd, syslogd等后台进程,并分别有自己的运行账户mysql, apache, syslog。‘
  • Linux服务器通常时可配置的。服务器程序通常处理很多命令选项,如果一次运行的选项太多,则克拉一用配置文件来管理。绝大多数服务器程序都有配置文件并存放在/etc下
  • Linux服务器程序通常在启动时生成一个PID文件并存入/var/run目录中,以记录该后台进程的PID。
  • Linux服务器程序通常需要考虑系统资源和限制,以预测自身能承受多大负荷,比如进程可用文件描述符总数和内存总量等。

1.日志

1.Linux系统日志:

  • Linux提供一个守护进程来处理系统日志–syslogd, 升级版–rsyslogd。
  • rsyslogd守护进程可以接收用户进程输出日志,可以接受内核日志。
  • 用户进程时通过调用syslog函数生成系统日志的。
  • 该函数将日志输出到一个unix本地域socket类型(AF_UNIX)的文件/dev/log中,rsyslogd则监听该文件以获取用户进程的输出。
  • 内核日志在以前的系统上时通过另一个守护进程rklogd来管理的,rsyslogd利用额外的模块实现了相同的功能。内核日志由printk等换树打印至内核环状缓存中。环状缓存的内容直接映射到/proc/kmsg。
  • rsyslogd通过读取该文件获得内核日志,默认调试信息保存在/var/log/debug,普通信息保存至/var/log/messages,内核信息:/var/log/kern.log。配置文件:/etc/rsyslog.conf,主要设置内核日志输入路径,是否接受UDP日志,及其监听端口(默认514 /etc/services)是否接受TCP日志及其监听端口,日志文件权限,包含哪些配置文件。
    这里写图片描述

2.syslog()

应用程序使用syslog()与守护进程rsyslogd通信。

#include
void syslog( int priotity, const char * message, ***);

该函数采用可变参数(第二个参数message和第三个参数。。。)来结构化输出。
priority:设施值 (按位异或) 日志级别。设施值默认:LOG_USER,下面针对默认设施值,讨论日志级别。

#include

#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    6   //信息
#define LOG_DEBUG   7   //调试

1.下面这个函数可以改变syslog的默认输出方式,进一步结构化日志内容

#include
void openlog(const char * ident, int logopt, int facility);

(1)ident:指定字符串将被添加到日志消息的日期和时间之后,通常设为程序的名字。
(2)logopt:对后续syslog调用的行为进行配置,它可取下列值的按位异或

#define LOG_PID     0x01    //日志消息包含PID
#define LOG_CONS    0x02    //如果消息不能记录到日志文件,则打印到终端
#define LOG_ODELAY  0x03    //延迟打开日志功能直到第一次调用syslog
#define LOG_NDELAY  0x04    //不延迟

(3)facility: 用来修改ysyslog默认设施值

此外,日志过滤也很重要,程序再开发阶段可能需要输出很多调试信息,而发布之后,我们又要将这些调试信息关闭,解决这个问题的方法并不是再程序发布之后,删除调试代码(日后可能还会用到),而是缉拿但地设置日志掩码,使日志级别大于日志掩码的日志被系统忽略。

2.下面这个函数用于设置syslog的日志掩码。

#include
int setlogmark( int maskpri );

maskpri:指定日志掩码值,该函数始终回成功,它返回调用进程先前的日志掩码值。

3.关闭日志功能:

#include
void closelog();

2.用户信息

1.UID, EUID, GID, EGID

用户信息对于服务器安全很重要,大多说服务器以root启动, 非root运行

Tables Are get & set
UID 真实用户ID getuid() / setuid(uid_t uid)
EUID 有效用户ID geteuid() / setuid(uid_t uid)
GID 真实组ID getgid() / setuid(gid_t gid)
EGID 有效组ID getgeid() / setuid(gid_t gid)

基础知识:

一个进程拥有两个用户ID, UID, EUID, EUID存在的目的是为了方便资源的访问, 它使得运行程序的用户拥有该程序的有效用户权限,比如,su用来更改账户信息,但修改账户时su程序的所有者是root,在普通用户运行su程序时,其有效用户就是该程序的所有者root, 有效用户为root的进程称为特权进程,EGID与EUID类似,下面演示uid, euid区别:

#include
#include

int main()
{
    uid_t uid = getuid();
    uid_t euid = geteuid();
    printf("uid is %d ,  euid is %d \n", uid, euid);
    return 0;
}

将生成的可执行文件,所有者设置为root,并设置该文件set-user-id标志,然后运行。

从测试输出结果看,进程的uid是启动程序的用户id, 而euid是root。

2.切换用户

static bool switch_to_user(uid_t user_id, gid_t gp_id)
{
    if((user_id == 0) && (gp_id == 0))
        return false;

    gid_t gid = getgid();
    uid_t uid = getuid();
    if( ( (gid != 0) || (uid!= 0)) && (( gid != gp_id) || ( uid != user_id )) )
        return false;

    if(uid != 0) 
        return false;
    if((setgid(gp_id) < 0) || setuid(user_id) < 0))
        return false;

    return true;

}

3.进程间关系

1.进程组:

linux下每一个进程都属于一个进程组,因此他们除了pid之外,还有进程组ID(PGID)。我们用如下函数获取指定进程组PGID.

#include
pid_d getpgid(pid_t pid);

成功返回pid, 失败-1,设置errno。
如果pid与pgid相同,则由pid指定的进程别设置为进程组首领:如果pid为0, 表示当前进程的PGID为pgid;如果pgid为0, 则使用pid作为目标pgid。setpid函数成功时返回0, 失败-1, 设置errno。
一个进程只能设置自己或者其子进程的PGID。并且, 当子进程调用exec系列函数后,我们也不能再在父进程中对他设置PGID。

2.会话

(1)一些有关联的进程将组成一个会话, 下面的函数用于创建一个会话:

#include
pid_t setsid(void);

该函数不能由进程组的首领进程调用,否则将产生一个错误。对于非首领的进程, 调用该函数不仅创建新会话, 而且有如下额外效果。
调用进程成为会话的首领,此时该进程时新会话的唯一成员。
新建一个进程组,其PGID就是调用进程的PID, 调用进程成为该组的首领。
调用进程将甩开终端(如果有)

该函数成功时返回新的进程组PGID, 失败-1, errno。
Linux进程并未提供所谓会话ID的概念, 但Linux系统认为它等于会话首领所在的进程组的PGID,
(2)并提供了如下函数读取SID

#include
pid_t getsid(pid_t pid);

3.用ps命令查看进程关系

执行ps命令可查看进程,进程组和会话之间的关系。

这里写图片描述
这里写图片描述

在bash_shell 下执行ps和less命令,所以ps和less命令的父进程时bash命令,这个可以从PPID(父进程PID)一列看出。
这三条命令创建了一个会话(SID是2962)和两个进程组(PGID:2962, 3102)bash命令的PID,PGID和SID都相同,显然它时会话的首领, 也就是组2962的首领。ps时3102的首领,
这里写图片描述

4.系统资源限制

Linux上运行的程序都会受到资源限制的影响,比如物理设备限制(cpu数量,内存数量等),系统策略限制(cup时间等),以及具体实现的限制(文件名最大长度)Linux系统资源限制可以通过如下一对函数来读取和设置:
getrlimit , setrlimit

#include
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;
};

成功返回0, 失败-1, 置errno

rlim_t 是一个整数类型,它描述资源级别

rlim_cur 成员指定资源的软限制,建议性的,最好不要超越的限制,如果超越,系统可能向进程发送信号,并终止运行,如果当前进程CPU时间超过软限制,系统将向进程发送SIGXCPU信号;当文件尺寸超过其软限制时,系统将向进程发送SIZEXFSZ信号。

rlim_max 成员指定资源的硬限制。硬限制一般是软限制的上限,普通程序可以减小应限制,而只有以root身份运行的程序才能增加硬限制,此外我们可以使用ulimit命令修改当前shell环境下的资源限制(软/硬)这种修改对该shell启动的所有后续程序都有效,我们也可以通过修改配置文件来改变系统软限制和应限制,而这种修改时永久的。

resource参数指定资源限制类型。如下表

资源限制类型 含义
RLIMIT_AS 进程虚拟内存(单位字节)超过该限制将使得某些函数(mmap)产生ENOMEN错误
RLIMIT_CORE 进程核心转储文件(core dump)的大小限制(单位字节)其值为0表示不产生核心转储文件
RLIMIT_CPU 进程CPU时间限制(单位秒)
RLIMIT_DATA 进程数据段(data, bss, 堆)限制(单位字节)
RLIMIT_FSIZE 文件大小限制(单位字节)超过该限制将是的某函数(write)产生EFBIG错误
RLIMIT_NOFILE 文件描述符数量限制,超过该限制将是的某些函数(比如pipe)产生MEFILE错误
RLIMIT_NPROC 用户能创建的进程数限制,超过该限制使得某些函数(比如fork)产生EAGIN错误
RLIMIT_STACK 进程栈内存限制(单位字节)超过该限制将引起SIGSEGV信号

5.改变工作目录和根目录

有些服务器程序好需要改变工作目录和根目录(web /var/www)
获取当前进程工作目录和改变进程的工作目录的函数:

#include
char * getcwd(char * buf, size_t size);
int chdir(const char * path);

buf参数指向的内存用于存储当前工作目录的绝对路径,size指定其大小
如果当前目录的绝对路径超度(+1 (‘\0’))超过了size,则getcwd返回NULL,errno:ERANG。

chdir中path指向要切换到的目录。成功0, 失败-1 置errno。

改变进程根目录:chroot

#include
int chroot(const char * path);

chroot并不改变进程的当前工作目录,调用chroot之后,仍需要调用chdir(“/”)来将工作转至新的工作目录,之后原来的文件描述符依然生效。所以可以利用早先打开的文件描述符来访问调用chroot之后不能直接访问的文件(目录).

6.服务器程序后台化

最后,如何在代码中让一个进程以守护进程的防止运行,守护进程的编写遵循一定的步骤,下面一个实例。

bool daemonize()
{
    pid_t pid = fork();//创建子进程
    if(pid < 0)
    {
        return false;
    }
    else if( pid > 0 )
    {
        exit(0); //父进程退出
    }

    pit_t sid = setsid();
    if(sid < 0)
    {
        return false;
    }

    if(chdir("/") < 0) //更改进程工作目录为根
    {
        return false;
    }
//重定向输出输出,错误输出
    close(STDIN_FILENO); 
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    open("/dev/null", O_RDONLY);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);
    return true
}

实际上,linux提供了完成同样功能的库函数:

#include
int deamon(int nochdir, int noclose);

nochdir:传0则工作目录将被设置为”/”,否则继续使用当前工作目录。
noclose:传0标准输入输出,标准错误输出都被重定向到,dev/null,否则继续使用原来的设备,成功0, 失败-1 置error。

最后 ,暑假开始了!Let’s play!

你可能感兴趣的:(Linxu网络编程)