文章目录
一、日志小组件
日志:程序运行过程中记录的程序运行状态信息
作用:记录了程序运行状态信息,以便程序猿能够随时根据状态信息,对系统程序的运行状态进行分析。能让用户非常简便的进行详细的日志输出以及控制
日志等级
在小组件中我们将日志分为6个等级,以便我们对信息进行分类
// 日志等级
#define DEBUG 0
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define UNKNOW 5
日志格式
日志输出格式: 日志等级 时间 文件名称 行号 PID 消息体
格式化日志等级
方法一:使用函数
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 "FATAL";
default : return "UNKNOW";
}
}
我们可以通过构建函数进行数组和字符串的转换来获取日志等级
格式化输出时间
static std::string getTime() {
// int tm_sec; /* Seconds. [0-60] (1 leap second) */
// int tm_min; /* Minutes. [0-59] */
// int tm_hour; /* Hours. [0-23] */
// int tm_mday; /* Day. [1-31] */
// int tm_mon; /* Month. [0-11] */
// int tm_year; /* Year - 1900. */
// int tm_wday; /* Day of week. [0-6] */
// int tm_yday; /* Days in year.[0-365] */
// int tm_isdst; /* DST. [-1/0/1]*/
time_t curr = time(nullptr);
struct tm *tmp = localtime(&curr);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_yday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
return buffer;
}
这里我们使用了time(), localtime()两个函数
time
函数用于获取当前时间的时间戳
time_t time(time_t *t);
然而时间戳是一个非常大的整数,不利于人类识别时间,我们要使用localtime()
函数将时间戳转换成可读状态
struct tm *localtime(const time_t *timep);
struct tm
类型的定义如下,我们会使用到tm_year, tm_mon, tm_yday, tm_hour, tm_min, tm_sec
需要注意的是该函数的年是从1900年开始算的,月份是从0 - 11月,我们使用时需要将年份加上1900,将月份加上1
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
这里我们还使用了snprintf()函数,其使用方法和printf非常像,printf是向标准输出文件中写,而snprintf是向指定缓冲区中写
int snprintf(char *str, size_t size, const char *format, ...);
获取文件名称,行号
我们可以使用C语言为我们提供的__FILE__和__LINE__
两个宏来获取文件名称和行号
printf("%s:%d\n", __FILE__, __LINE__);
[clx@VM-20-6-centos thread_pool_tcp_2.0]$ ./clx_test
test.cpp:4
用户消息处理
用户传入的消息是一个可变参数列表 ,下面我们先解释一下可变参数列表
可变参数列表
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
我们可以用一段代码来理解这一系列借口的使用
#include <cstdio>
#include <cstdarg>
#define LOG(fmt, ...) printf("[%s : %d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);
void printNum(int count, ...) { // count 不定参数的个数
va_list ap; // va_list实际就是一个char*类型的指针
va_start(ap, count); // 将char*类型指针指向不定参数的起始位置
for (int i = 0; i < count; i++) {
int num = va_arg(ap, int); // 从ap位置取一个整形大小空间数据拷贝给num,并将ap向后移动一个整形大小空间
printf("param[%d], %d\n", i, num);
}
va_end(ap); // 将ap指针置空
}
int main() {
printNum(2, 666, 222);
return 0;
}
- va_list ap: 就是定义一个char* 类型的指针
- va_start : 让指针指向不定参数的起始位置,第二个参数传的是不定参数的前一个参数,因为函数调用过程中是会将实参一个个压入函数栈帧中的,所以参数之间都是紧挨着的。我们找到了前一个参数count的地址,也就等于找到了不定参数的起始地址
- va_arg : 用于从不定参数中解析参数,第一个参数数据的起始位置,第二个参数指定参数类型,根据类型我们可以推导出参数的大小,从而将参数数据解析出来
- va_end : 将ap指针置空
这里我们解释传入类型只能是int类型,我们如何使用上述接口将不定参数分离的原理,那么printf这类函数是如何将不定参数分离的呢??**这是因为我们在使用printf函数开始传递了format
参数,其中包含了%s, %d
这类的信息,printf内部通过对format
参数进行解析就知道了后面的参数依次都是什么类型的,然后将类型依次放入va_arg函数,就可以将参数全部提取出来了
void myprintf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
char* res;
int ret = vasprintf(&res, fmt, ap);
if (ret != -1) {
printf(res);
free(res);
}
va_end(ap);
}
- vasprintf 函数会帮助提取不定参数并且将其拼接到格式化字符串中,并开辟空间将处理好的字符串数据放入空间,并将我们传入的指针指向这块空间
- 成功返回打印的字节数,失败返回-1
vsnprintf()函数
这里我们选择使用vsnprintf()
函数来处理我们的不定参数,我们只需要获取不定参数的起始位置,该函数就会自动帮我们根据 format取出不定参数进行拼接,然后输出到我们指定的位置
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
char logRight[1024]; // 用于获取格式化字符串
va_list p;
va_start(p, format);
vsnprintf(logRight, sizeof(logRight), format, p);
va_end(p);
这样我们就成功获取到用户描写的输出信息了
拼接信息写入文件
这里我们使用C语言的方式进行文件写入,以append的方式写入数据
FILE *fp = fopen(filename.c_str(), "a+");
if (fp == nullptr) {
return;
}
fprintf(fp, "%s%s\n", logLeft, logRight);
fflush(fp); // 如果把文件放在这可以不写,因为马上就要关闭文件了。如果将文件放在tcp_server 的成员变量中,因为不会重复打开和关闭,所以就需要刷新
fclose(fp);
日志组件代码
#pragma once
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <ctime>
#include <string>
#include <unistd.h>
#include <sys/types.h>
static const std::string filename = "log/tcp_server.log";
// 日志等级
#define DEBUG 0
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define UNKNOW 5
// 日志格式: 日志等级 时间 文件 行 PID 消息体
// logMessage(DEBUG, "hello : %d, %s", 12, s.c_str()); /
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 "FATAL";
default : return "UNKNOW";
}
}
static std::string getTime() {
// int tm_sec; /* Seconds. [0-60] (1 leap second) */
// int tm_min; /* Minutes. [0-59] */
// int tm_hour; /* Hours. [0-23] */
// int tm_mday; /* Day. [1-31] */
// int tm_mon; /* Month. [0-11] */
// int tm_year; /* Year - 1900. */
// int tm_wday; /* Day of week. [0-6] */
// int tm_yday; /* Days in year.[0-365] */
// int tm_isdst; /* DST. [-1/0/1]*/
time_t curr = time(nullptr);
struct tm *tmp = localtime(&curr);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_yday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
return buffer;
}
static 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(), getpid());
char logRight[1024];
va_list p;
va_start(p, format);
vsnprintf(logRight, sizeof(logRight), format, p);
va_end(p);
// 直接打印到显示屏上
// printf("%s%s\n", logLeft, logRight);
// 保存到文件中
FILE *fp = fopen(filename.c_str(), "a+");
if (fp == nullptr) {
return;
}
fprintf(fp, "%s%s\n", logLeft, logRight);
fflush(fp); // 如果把文件放在这可以不写,因为马上就要关闭文件了。如果将文件放在tcp_server 的成员变量中,因为不会重复打开和关闭,所以就需要刷新
fclose(fp);
二、守护进程
守护进程即常说的 Daemon 进程,是 Linux 中的后台服务进程,周期性地执行某种任务或者等待某些发生的事件。
我们在终端中可以使用./
的方式让程序在前台运行,也可以在后面加上一个&
让程序在后台运行,并可以使用jobs
来查看后台运行的进程。也可以使用Ctrl + z
快捷键让程序停止并放置后台
jobs 指令
-
jobs -l
: 查看后台进程 -
bg + %n
: 让后台停止进程继续运行 -
fg
+ %n(编号) : 将该进程提到前台运行
Linux会话概念
会话(session)是若干进程的集合,系统中的每一个进程也必须从属于某一个会话。 一个会话最多只有一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共有。 一个会话中只会有一个前台进程组,只有前台进程组中的进程才可以和控制终端进行交互 在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell)
当我们启动一个程序,可以使用ps
指令查看这个进程的一些属性信息
[clx@VM-20-6-centos ~]$ sleep 10000 | sleep 20000 | sleep 3000 &
[1] 28705
[clx@VM-20-6-centos ~]$ ps -axj | head -1 && ps -axj | grep sleep | grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1212 953 1130 1130 ? -1 S 0 0:00 sleep 60
7094 7331 7094 1177 ? -1 Sl 0 11:57 /bin/sh -c sleep 100
31712 31868 31868 31712 pts/0 1172 S 1001 0:00 sleep 10000
31712 31869 31868 31712 pts/0 1172 S 1001 0:00 sleep 20000
31712 31870 31868 31712 pts/0 1172 S 1001 0:00 sleep 30000
首先我们先解释一下各个属性的名称
- PPID : 父进程的PID
- PID : 进程的PID
- PGID :process group id 进程组ID
- SID : 会话ID
- TTY : 终端关联文件 打问号的说明该进程和终端没有关系,
pts/0
是终端文件,可以使用ls /dev/pts/16 -al
查看终端文件
我们使用|
分隔的的程序之间实际是通过匿名管道通信的,所以他们三条程序启动后就是三个进程,前为后的父进程。可以看到这三个进程的PPID相同(因为他们的父亲都是我们的bash), 我们可以使用指令验证一下
[clx@VM-20-6-centos ~]$ ps -axj | grep 31712
31712 1492 1492 31712 pts/0 1492 R+ 1001 0:00 ps -axj
31712 1493 1492 31712 pts/0 1492 S+ 1001 0:00 grep --color=auto 31712
31711 31712 31712 31712 pts/0 1492 Ss 1001 0:00 -bash
31712 31868 31868 31712 pts/0 1492 S 1001 0:00 sleep 10000
31712 31869 31868 31712 pts/0 1492 S 1001 0:00 sleep 20000
31712 31870 31868 31712 pts/0 1492 S 1001 0:00 sleep 30000
然后我们发现这三个线程属于同一个线程组,并且三个线程也拥有相同的会话ID,我们还可以发现会话ID的值和bash进程的PID相同,进程组的ID和进程组内第一个进程的ID相同
实际上我们的终端链接我们的Linux服务器系统时,Linux内核会给我们创建一个会话,我们的所有的进程呀,线程呀都在这个会话中运行。
而我们使用Xshell链接云服务器的本质就是,内核给我们创建一个会话,而其中开始运行的第一个进程也就是我们的bash,所以会话ID和bash的ID是相同的。而bash 又可以创建很多的子程序,这些程序都运行在这个会话中,而我们的终端本质就是一个文件,其就在/dev/pts
中
[clx@VM-20-6-centos ~]$ ls /dev/pts
10 11 12 13 14 15 16 0 5 6 7 8 9 ptmx
[clx@VM-20-6-centos ~]$ ls /dev/pts/16 -al
crw--w---- 1 clx tty 136, 16 7月 17 23:34 /dev/pts/16
如果后台任务提到前台,老的前台任务无法运行在会话中只能有一个前台任务正在运行! – 我们命令行启动一个进程后,bash就无法运行了。如果登陆就是创建一个会话,bash任务,启动我们的进程,就是在当前会话中创建新的前后台任务,那么如果我们退出呢。销毁会话可能会影响会话内所有任务,不同操作系统的操作不同,所以一般网络服务器需要以守护进程的方式运行
企业的服务器肯定不能受终端的退出而影响,我们要保证其100%概率正确运行。所以这些服务器一般都以守护进程的方式运行在操作系统中
创建独立会话
要想我们的程序不被当前会话影响,方法就是让这个程序脱离当前会话,自己成为一个独立的会话。比如我们想要让我们的TCP服务器成为一个守护进程该怎么办呢?
setsid()
:谁调用这个函数,谁就成为会话,SID就等于这个进程的PID。但是规定调用的进程不能是组长。
pid_t setsid(void);
我们的TCP服务器就只有一个进程,它肯定就是组长了,如何让其成为组员呢??
只需要创建一个进程,然后让它的子进程运行我们的TCP服务器即可,父进程我们就直接让其退出。这样我们进程就会被爷爷进程也就是1号进程领养。这时候我们就不是组长了,就可以调用setsid()
函数了
所以守护进程的本质就是孤儿进程的一种。所以守护进程是直属操作系统的会话,那么我们在守护进程中调用cin,cout等函数我们的终端就收不到了,我们可以选择强制关闭0, 1, 2文件,但是这样过于暴力,如果我们的代码中出现了cin或者是cout的代码会直接报错
我们最常见的选择是将使用系统给我们提供的/dev/null
设备,其是一个字符设备,类似于垃圾桶,会自动丢弃掉向其写入的任何信息,我们可以从其内部读取到随机数。我们将0, 1, 2都重定向到这个字符设备就可以了
守护进程组件代码
#pragma once
#include "log.hpp"
#include "err.hpp"
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
// 守护进程的本质 : 是孤儿进程的一种
void Daemon() {
// 1、 ignore signal
signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN);
// 2、 let father process quit
if (fork() > 0) exit(0);
// 3、 new session , child process become the first process
pid_t ret = setsid();
if (static_cast<int>(ret) == -1) {
logMessage(FATAL, "deamon error, code: %d, string: %s", errno, strerror(errno));
exit(SETSID_ERR);
}
// 4、 更改守护进程的工作路径
// 5、 处理后续的对于0,1,2 的问题
// 直接强制关闭0, 1, 2 过于暴力,如果代码中出现cin,cout则会直接终止进程
// 将012重定向到垃圾桶, 将输出的东西丢掉垃圾桶(/dev/null)
int fd = open("/dev/null", O_RDONLY | 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);
}