日志
一般使用cout进行打印,但是cout打印是不规范的 实际上 是采用日志进行打印的
日志的创建
创建一个 log.hpp
日志有自己的日志等级
通过枚举,分别为 调试 常规 告警 一般错误 致命错误 未知错误
logmessage 函数
定义一个函数 logmessage,参数level 为日志等级 , 为了按照可变参数的方式,来进行格式化输出,所以设置一个format 以及...可变参数(可以给c函数传递任意个数的参数)
日志左边部分实现
输入 man snprintf
将可变参数的内容显示到str字符串中
获取日志等级
设置一个字符串 level_string ,通过tolevelstring函数 将数字转化为字符串
获取时间
输入 man localtime
将time_t转换为 struct tm 结构体类型
该结构体包含 秒 分 时 天
输入 man 3 time
通过gettime函数 获取时间
日志右边部分实现
为了处理可变参数部分,所以使用vsprintf 输入 man snprintf
将写好的数据放到logRight中
完整代码
log.hpp(整体实现)
#pragma once #include<iostream> #include<string.h> #include<cstdio> #include<cstring> #include<cstdarg> #include<unistd.h> #include<sys/types.h> #include<time.h>
const std::string filename="tecpserver.log";
//日志等级
enum{
DEBUG=0, // 用于调试
INFO , //1 常规
WARNING, //2 告警
ERROR , //3 一般错误
FATAL , //4 致命错误
UKNOWN//未知错误
};static std::string tolevelstring(int level)//将数字转化为字符串
{
switch(level)
{
case DEBUG : return "DEBUG";
case INFO : return "INFO";
case WARNING : return "WARNING";
case ERROR : return "ERROR";
case FATAL : return "TATAL";
default: return "UKNOWN";
}
}
std::string gettime()//获取时间
{
time_t curr=time(nullptr);//获取time_t
struct tm *tmp=localtime(&curr);//将time_t 转换为 struct tm结构体
char buffer[128];
snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,
tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;}
void logmessage(int level, const char*format,...)
{
//日志左边部分的实现
char logLeft[1024];
std::string level_string=tolevelstring(level);
std::string curr_time=gettime();
snprintf(logLeft,sizeof(logLeft),"%s %s %d",level_string.c_str(),curr_time.c_str());//日志右边部分的实现
char logRight[1024];
va_list p;//p可以看作是1字节的指针
va_start(p,format);//将p指向最开始
vsnprintf(logRight,sizeof(logRight),format,p);
va_end(p);//将指针置空//打印日志
printf("%s%s\n",logLeft,logRight);
//保存到文件中
FILE*fp=fopen( filename.c_str(),"a");//以追加的方式 将filename文件打开
//fopen打开失败 返回空指针
if(fp==nullptr)
{
return;
}
fprintf(fp,"%s%s\n",logLeft,logRight);//将对应的信息格式化到流中
fflush(fp);//刷新缓冲区
fclose(fp);
}
err.hpp (错误信息枚举)
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,//2
BIND_ERR,//3
LISTEN_ERR,//4
SETSID_ERR,//5
OPEN_ERR//6
};
守护进程
网络服务一定在任何时候都能访问,所以这个服务不能受任何用户的登录或者注销各种行为的影响 所以需要将进程进行守护进程化
PGID SID TTY 的介绍
在后台运行sleep 10000
PPID是bash的PID值 PGID是 进程组 (PGID相同就为同一个进程组,以从第一个进程进行命名) SID 是 会话ID TTY是 终端 若为?,则说明跟终端没有关系,若为具体的如pts/5,则为终端文件
在终端2中输入,在终端1中可以查看到 两者的PGID相同,所以属于同一个进程组,并且以sleep 1000 作为组长
通过查询会话ID 21668,发现bash的PID PGUD SID 都为21668
shell中控制进程组的方式
查询后台任务 jobs
当再次输入sleep 5000 进行后台运行时,发现前面的编号变成2 该编号为 任务编号
将某一任务提到前台运行 fg + 任务编号
当把1号任务提到前台后,再次使用jobs查询后台任务,就查不到1号任务了 并且其他任务并不受影响
把2号任务提到前台,使用 ctrl z 让服务暂停起来 在暂停后,任务会自动切换到后台
输入 bg 2,让2号任务在后台跑起来
结论
1. 进程组分为 前台任务 和 后台任务
在终端2中创建后台任务和前台任务,在终端1中查询发现,后台任务的(PGID)进程组 和 (SID)会话ID相同 ,而与后台的不同
2. 如果后台任务提到前台,老的前天任务就无法运行
将任务编号为1的后台任务 使用 fg 提到前台后 ,输入 ls pwd 等 指令是没有作用的 会话中 ,只能有一个前台任务在运行 所以当 使用 ctrl c 将1号任务退出后,bash把自己变成了前台任务,所以又可以运行了
为什么要有守护进程存在?
若登录就是创建一个会话,启动进程,会话内部有bash任务,在当前会话中创建新的前后台任务,那如果退出呢?
当退出时,就会销毁会话可能会影响会话内部的所有任务
网络服务器为了不受到用户登录注销的影响,网络服务器 通常以守护进程的方式运行
守护进程的创建
输入 man 2 setsid
设置一个会话,以进程组的组长ID作为新的会话ID
若返回成功,则返回调用进程的PID,若返回失败,则返回-1并设置错误码
想要调用setsid,不可以是组长
如:在一家公司中你是组长,有一天你想不干了 出去创业 是不可以的,因为你手底下有一堆组员 所以要成功出去创业,就必须卸任你的组长身份
使用守护进程的条件
1.忽略异常 2.对 0(标准输入) 1(标准输出) 2(标准错误) 作特殊处理 3.进程的工作路径 可能要更改 4.守护进程是一个全局的进程,不想在某一个用户的目录下,所以从整个系统中从最开始进行索引某些文件
守护进程化的函数
输入 man daemon,提供守护进程化的函数
第一个参数表示 是否更改 工作目录,默认不要改,改为1表示为真 第二个参数表示 要不要关闭 0 1 2, 默认不关
大部分情况下,都是自己实现守护进程,而不是调用该函数
自己实现守护进程化
解决组长问题
当启动时,是在bash中新起一个任务,只有一个进程自成进程组,所以自成组长,操作不被允许
成为组长的一般都是组中的第一个进程,所以只需使其不为第一个进程即可
输入 man fork,创建子进程
fork的返回值:父进程返回子进程的PID值,子进程返回0,失败返回-1
当fork>0时,说明为父进程,则让父进程退出,只剩下子进程,子进程不是进程的第一个,也就不是组长,就可以成功调用setsid
忽略信号
signal的第一个参数 表示 信号 ,第二个参数表示对指定动作的信号设定自定义处理动作
SIGPIPE 表示13号信号
SIG_IGN 为 自定义处理信号处理函数
把1强制转化成函数指针类型 即忽略信号
对13号信号 进行忽略
SIGCHLD信号 子进程在运行时会退出,若父进程不关心子进程退出,子进程就会变成僵尸状态 父进程要使用 wait/waitpid去等待子进程 回收僵尸,获取子进程的退出结果 即父进程进行阻塞式等待(什么都不干,就等待子进程的退出结果) 子进程要退出时,会向父进程发信号 SIGCHLD
所以同样对 SIGCHLD信号 进行忽略
处理 0 1 2 问题
使用日志打印,所以导致有很多输出结果,但输出结果不想往显示器上面打印,所以就需要处理标准输入 标准输出 标准错误
Linux系统提供一个 dev null的字符设备
向dev null 中写入,都会被丢弃 ,从这个文件读什么都读不到 ,立马直接返回
输入 man 2 open,打开文件
若返回成功,则返回 文件描述符,若返回失败,则返回 -1 并将错误码返回 O_RDWR : 读写的方式
重定向函数 :输入 man dup2
可以直接将文件打开,使用dup2重定向 输出重定向对应的文件描述符是1 假设其文件描述符是fd newfd为oldfd的一份拷贝,最后只剩下oldfd dup2(fd,1) 即 将标准输出流 重定向到 文件描述符fd中
退出守护进程
输入 kill -9 + 守护进程的PID,即可退出守护进程
完整代码
err.hpp(错误信息枚举)
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,//2
BIND_ERR,//3
LISTEN_ERR,//4
SETSID_ERR,//5
OPEN_ERR//6
};
daemon.hpp(整体实现)
#pragma once
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include"log.hpp"
#include"err.hpp"void Daemon()//自己实现服务器的守护进程化
{
//1.忽略信号
signal(SIGPIPE,SIG_IGN);//忽略信号
signal(SIGCHLD,SIG_IGN);//2.不要成为组长 if(fork()>0)//说明为父进程,则让父进程直接退出 { exit(0); } //只剩下子进程 //3.新建会话,自己成为会话的话首进程 pid_t ret=setsid(); if((int)ret==-1)//守护进程失败 { logmessage(FATAL,"deamon error,code:%d,string :%s",errno,strerror(errno)); exit(SETSID_ERR);//终止程序 } //4.可以更改守护进程的工作路径 //5.处理 0 1 2 问题 int fd=open("/dev/null",O_RDWR);//以读写的方式打开字符设备 if(fd<0) { logmessage(FATAL,"deamon error,code:%d,string :%s",errno,strerror(errno)); exit(OPEN_ERR);//终止程序 } //将标准输入 输出错误 重定向到字符设备中 dup2(fd,0); dup2(fd,1); dup2(fd,2); close(fd);
}