日志
Linux提供一个守护进程来处理系统日志【syslogd】,现在用的是它的升级版【rsyslogd】
守护进程:Linux服务器程序一般以后台程序的形式运行,后台进程也叫守护进程。它没有控制终端,所以不会意外接收到用户输入。
守护进程的父进程通常是init进程【PID为1的进程】
rsyslogd守护进程既能接收用户进程输出的日志,又能接收内核日志
用户进程通过调用syslog函数生成系统日志。该函数将日志输出到一个UNIX本地域socket类型(AF_UNIX)的文件/dev/log中,rsyslogd监听该文件以获取用户进程的输出。
内核日志在老的系统上是通过另外一个守护进程rklogd来管理的,rsyslogd利用额外的模块实现了相同的功能。内核日志由printk等函数打印至内核的环状缓存(ring buffer)中。环状缓存的内容直接映射到/proc/kmsg文件中,rsyslogd通过读取该文件获得内核日志
rsyslogd守护进程在接收到用户进程或内核输人的日志后,会把它们输出至某些特定的日志文件。
默认情况下:
调试信息保存至/varlog/debug文件
普通信息保存至/var/log/messages文件
内核消息保存至/var/log/ker.log文件
日志信息具体如何分发,可以在rsyslogd的配置文件中设置。rsyslogd 的主配置文件是/etc/rsyslog.conf
主要可以设置的项包括:内核日志输入路径,是否接收UDP日志及其监听端口(默认是514,/etc/services文件),是否接收TCP日志及其监听端口,日志文件的权限,包含哪些子配置文件(比如/etc/rsyslog.d/* .conf)。rsyslogd的子配置文件则指定各类日志的目标存储文件
syslog函数
应用程序使用syslog函数和rsyslogd守护进程通信:
#include<syslog.h>
void syslog(int priorty,const char* massage,...);
该函数采用可变参数结构化输出
priority参数是所谓的设施值与日志级别的按位或
设施值的默认值是LOG_USER
日志级别:
-#define LOG_EMERG 0//系统不可用
-#define LOG_ALERT 1//报警,需立刻采取动作
-#define LOG_CRIT 2//非常严重的情况
-#define LOG_ERR 3//错误
-#define LOG_WARNING 4//警告
-#deine LOG_NOTICE 5//通知
-#deinfe LOG_INFO 6//信息
-#define LOG_DEBUG 7//调试
改变syslog的默认输出方式,进一步结构化日志内容:
#include<syslog.h>
void openlog(const char* ident,int logpot,int facility);
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参数指定日志掩码值。始终会成功,它返回调用进程先前的日志掩码值
操作完成后记得关闭日志功能:
#include<syslog.h>
void closelog();
用户信息
用户信息对于服务器程序的安全性来说是很重要的,比如大部分服务器就必须以root身份启动,但不能以root身份运行
UID、EUID、GID、EGID
UID:真实用户ID
EUID:有效ID
GID:真实组ID
EGID:有效组ID
获取/设置当前进程的各ID:
一个进程拥有两个用户ID: UID和EUID
UID是启动程序的用户的IDEUID是root(文件所有者)账户的ID,存在的目的是方便资源访问:它使得运行程序的用户拥有该程序的有效用户的权限。比如su程序,任何用户都可以使用它来修改自己的账户信息,但修改账户时su程序不得不访问/etc/passwd文件,而访问该文件是需要root权限的。那么以普通用户身份启动的su程序如何能访问/etc/passwd文件呢?窍门就在EUID。用Is命令可以查看到,su程序的所有者是root,并且它被设置了set-user-id标志。这个标志表示,任何普通用户运行su程序时,其有效用户就是该程序的所有者root。那么,根据有效用户的含义,任何运行su程序的普通用户都能够访问etc//passd文件。有效用户为root的进程称为特权进程
EGID的含义与EUID类似:给运行目标程序的组用户提供有效组的权限。
切换用户:
将以root身份启动运行的进程切换为以普通用户身份运行
static bool switch_to_user(uid_t user_id,gid_t gp_id)
{
//确保目标用户不是root
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;
//如果不是root,就已经是目标用户
if(uid!=0)
return true;
//切换到目标用户
if((setgid(gp_id)<0)||(setuid(user_id)<0))
return false;
return true;
}
进程间关系
进程组
Linux下每个进程都隶属于一个进程组,因此有进程组ID(PGID)
获取进程的PGID:
#include<unistd.h>
pid_t getpgid(pid_t pid);
成功时返回进程pid所属进程组的PGID,失败则返回-1并设置errno
每个进程组都有一个首领进程,其PGID和PID相同。进程组将一直存在,直到其中所有进程都退出,或者加入到其他进程组
设置PGID:
int setpgid(pid_t pid,pid_t pgid);
将PID为pid的进程的PGID设置为pgid
- 如果pid和pgid相同,则由pid指定的进程将被设置为进程组首领
- 如果pid为0,则表示设置当前进程的PGID为pgid
- 如果pgid为0,则使用pid作为目标PGID
- setpgid 函数成功时返回o,失败则返回1并设置errno
一个进程只能设置自己或者其子进程的PGID,当子进程调用exee系列函数后,不能再在父进程中对它设置PGID
会话
一些有关联的进程组将形成一个会话
创建会话:
#include <unistd.h> pid_t setsid(void)
该函数不能由进程组的首领进程调用,否则会产生错误。对于非组首领的进程,调用该函数不仅创建新会话,还会:
- 调用进程成为会话的首领,此时该进程是新会话的唯一成员
- 新建一进程组,其PGID就是调用进程的PID,调用进程成为该组的首领
- 调用进程将甩开终端
该函数成功返回新的进程组PGID,失败返回-1并设置errno
Linux进程没有提供会话ID(SID)的概念,但Linux系统认为它等于会话首领所在的进程组的PGID,并提供函数读取SID:
pid_t getsid(pid_t pid);
ps命令查看进程关系
ps -o pid,ppid,pgid,sid,comm|less
PPID:父进程ID
在bash shell下运行ps和less命令,所以ps、less的父进程是bash命令
三条命令创建一个会话【SID:1943】两个进程组:【PGID:1943、2298】
bash:PIDPGIDSID—>bash命令既是会话的首领也是组1943的首领
ps:PID==PGID---->ps命令是组2298的首领
系统资源限制
Linux上运行的程序都会受资源限制的影响,eg:物理设备限制(CPU数量,内存数量)、系统策略限制(CPU时间等)、具体实现的限制(文件名的最大长度)
Linux系统资源限制可通过下列函数读取、设置:
#include<sys/resource.h>
int getrlimit(int resource,struct rlimit *rlim);
int setrlimit(int resource,const struct rlimit *rlim);
//rlim是rlimit结构体类型的指针,定义:
struct rlimit
{
rlim_t rlim_cur;
rlim_t rlim_max;
};
- rlim_t:整数类型,描述资源级别
- rlim_cur:指定资源的软限制
- rlim_max:制定资源的硬限制
软限制是一个建议性的、最好不要超越的限制,如果超越的话,系统可能向进程发送信号以终止其运行。例如,当进程CPU时间超过其软限制时,系统将向进程发送SIGXCPU信号;当文件尺寸超过其软限制时,系统将向进程发送SIGXFSZ信号
硬限制一般是软限制的上限。普通程序可以减小硬限制,而只有以root身份运行的程序才能增加硬限制。此外,我们可以使用ulimit命令修改当前shell环境下的资源限制(软限制或/和硬限制),这种修改将对该shell启动的所有后续程序有效。我们也可以通过修改配置文件来改变系统软限制和硬限制,而且这种修改是永久的
- resource:指定资源限制类型
setlimit、getlimit成功返回0,失败返回-1并设置errno
改变工作目录和根目录
有的服务器需要改变工作目录和根目录
获取进程当前目录和改变进程工作目录:
#include<unistd.h>
char *getcwd(char *buf,size_t size);
int chdir(const char *path);
buf:指向的内存用于存储进程当前工作目录的绝对路径名,其大小由size参数指定
如果当前工作目录的绝对路径的长度(再加上一个空结束字符“\0”)超过了size,则getewd将返回NULL,并设置errno为ERANGE
如果buf为NULL并且size非0,则getewd可能在内部使用malloc动态分配内存,并将进程的当前工作目录存储在其中,此时必须自己释放getcwd在内部创建的这块内存
getcwd 函数成功时返回一个指向目标存储区(buf指向的缓存区或是getcwd在内部动态创建的缓存区)的指针,失败则返回NULL并设置errnoo
chdir函数的path参数指定要切换到的目标目录。它成功时返回0,失败时返回-1并设置errno.
改变进程根目录的函数是chroot:
int chroot(const char* path);
path:指定要切换到的目标根目录
它成功时返回0,失败时返回-1并设置errno
chroot并不改变进程的当前工作目录,所以调用chroot之后,仍然要用chdir(“/”)来将工作目录切换至新的根目录。改变进程的根目录之后,程序可能无法访问类似/dev的文件(目录),因为这些文件(目录)并非处于新的根目录之下。不过在调用chroot之后,进程原先打开的文件描述符依然生效,所以我们可以利用这些早先打开的文件描述符来访问调用chroot之后不能直接访问的文件(和目录),尤其是一些日志文件
注意:此外,只有特权进程才能改变根目录
服务器程序后台化
即让服务器程序以守护进程的方式运行:
bool daemonize()
{
//创建子进程,关闭父进程,让程序在后台运行
pid_t pid=fork();
if(pid<0)
return false;
else if(pid>0)
exit(0);
//设置文件权限掩码
//使用open(const char *pathname,int flags,mode_t mode)创建文件,文件权限:mode&0777
//创建新会话,设本进程为进程组首领
pid_t sid=setsid();
if(sid<0)
return false;
//切换工作目录
if((chdir("/"))<0)
return false;
//关闭标准输入设备、标准输出设备、标准错误输出设备
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;
}
Linux 提供了同样功能的库函数:
#include<unistd.h>
int daemon(int nochdir,int noclose);
nochdir用于指定是否改变工作目录,如果给它传递0,则工作目录将被设置为“1”(根目录),否则继续使用当前工作目录
noclose 参数为0时,标准输入、标准输出、标准错误输出都被重定向到/dev/null文件,否则依然使用原来的设备
该函数成功时返回0,失败则返回-1并设置errno