💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:Linux知识分享⏪
🚚代码仓库:Linux代码练习🚚
🌹关注我🫵带你学习更多Linux知识
🔝
目录
前言
书接上回,进程间通信我们利用管道可以通信,但是这些进程都是有血缘关系的进程,那有没有能让两个毫不相干的进程也能通信?有的,我们用命名管道,就能实现两个没有任何关系的进程进行通信。
命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用 FIFO 文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
指令: mkfifo filename
echo本来是输出到显示器的,我们加入重定向后,重定到filename这个命名管道文件中,
cat输入重定向从filename当中读取数据 hello world ,我们再ll命令发现filename文件大小还是0。f
那如何删除命名管道啊?
指令:unlink filename 或者 rm -f filename
这时你会想知道,命令管道如何在程序中如何创建?代码实现我们先不急,先理解两个没有任何关系的进程,它们进程间通信的底层原理。
从匿名管道中我们知道进程想要通信,首先就是要让两个进程看到同一份资源,匿名管道天然具有这种优势,子进程会继承父进程的所有资源。但是命名管道不是这样的,那如何让两个进程看到同一份资源?那就是两个进程打开同一个文件。那如何确保两个进程打开的是同一份文件?首先从OS层面来说,我管你是进程A还是进程B又或者进程C,你们三打开同一个文件,看似打开了三次,实际上OS为了效率,它只会打开一次这个文件,既然这样,那两个进程要通信,直接打开文件名相同的且路径相同文件就行了。为什么加路径?因为路径是唯一的。所以我们就能确保两个进程打开的是同一份文件。
这时有人就会问了,那我直接在两个进程中用系统调用open这个函数打开文件不就看到同一份资源了吗?一个进行读,一个进行写不就通信了吗?为什么还要用命名管道。首先两个进程从磁盘上读取,加载到内存,又从内存中写入,拷贝到磁盘中,这个过程就很慢,凡是和磁盘打交道,那就是慢慢慢!!!而刚才我们看到filename是没有字节的,这就意味着命名管道是不需要刷盘的,它是内存级的文件速度很快。
代码实现
实现之前我们需要先了解一个系统调用接口mkfifo
mkfifo
函数用于创建一个 FIFO (First-In-First-Out)特殊文件,也就是一个命名管道。以下是 mkfifo
函数的手册页翻译:
名称 mkfifo - 创建一个 FIFO 特殊文件(命名管道)
头文件
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
描述 mkfifo() 用指定的 pathname 创建一个 FIFO 特殊文件。mode 参数指定了 FIFO 的权限。文件的权限会受进程的掩码(umask)影响:创建的文件权限为 (mode & ~umask)。
FIFO 特殊文件类似于管道,但是创建方式不同。FIFO 特殊文件会通过调用 mkfifo() 进入文件系统。
一旦通过这种方式创建了一个 FIFO 特殊文件,任何进程都可以像操作普通文件一样打开它进行读写。但是,在进行任何输入输出操作之前,必须同时在两端打开它。尝试打开 FIFO 进行读取通常会阻塞,直到有其他进程打开相同的 FIFO 进行写入,反之亦然。参见 fifo(7) 中关于非阻塞处理 FIFO 特殊文件的内容。
返回值 mkfifo() 成功时返回 0。失败时返回 -1(此时 errno 被适当地设置)。
错误 EACCES pathname 中的一个目录不允许搜索(执行)权限。
EDQUOT 用户在文件系统上的磁盘块或索引节点配额已经耗尽。
EEXIST pathname 已经存在。这包括 pathname 是符号链接(悬空或非悬空)的情况。
ENAMETOOLONG
要么 pathname 的总长度超过了 PATH_MAX,要么其中一个文件名组件的长度超过了 NAME_MAX。在 GNU 系统中,文件名长度没有强制限制,但一些文件系统可能对文件名组件的长度施加限制。
ENOENT pathname 中的一个目录组件不存在或者是一个悬空的符号链接。
ENOSPC 目录或文件系统没有足够的空间用来创建新文件。
ENOTDIR
pathname 中的一个组件用作目录,但事实上不是目录。
EROFS pathname 指向的是一个只读文件系统。
cpp
#pragma once
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_FILE "./myFileName"
我们先自定义头文件comm.hpp这个头文件,我们在这里头文件创建命名管道 client.cc和server.cc这个两个源文件同时包含。就能看到同一个文件->命名管道。
创建命名管道
cpp
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_FILE "./myFileName"
#define MODE 0664
//退出码
enum
{
FIFO_CREATE_ERR = 1 //管道创建失败退出码设置为1
};
cpp
#include "comm.hpp"
using namespace std;
// 创建管道
int main ()
{
int n = mkfifo(FIFO_FILE,MODE);
if(n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
return 0;
}
我们用sever来试试能不能创建创建管道。
管道创建好了是不是就能进行通信了? 当然不行,我们现在只是让它们看到同一份资源。
那如何通信?别忘了linux下一切皆文件,当然我们的命名也是文件,那我们就可以利用之前的文件操作的那一套进行通信。open read write close 就是这么的简单。
下面我让client写,server读。
cpp
#include "comm.hpp"
using namespace std;
int main ()
{
int fd = open(FIFO_FILE,O_WRONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
//客户端写入
string line;
while(true)
{
cout << "Please Enter@ ";
getline(cin , line);
int x = write(fd,line.c_str(),line.size());
}
close(fd);
return 0;
}
cpp
#include "comm.hpp"
using namespace std;
// 创建管道
int main ()
{
//创建管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
//打开信道
int fd = open(FIFO_FILE,O_RDONLY);
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
//开始通信
while(true)
{
char buffer[1024] = {0};
int x = read(fd,buffer,sizeof(buffer));
if(x > 0)
{
buffer[x] = 0;
cout << "client say# " << buffer << endl;
}
}
close(fd);
return 0;
}
前面server写的创建管道不好,每次在命令端都要手动删除管道。下面进行优化,我们comm这个头文件中写一个创建管道的类 构造函数初始化管道,析构函数删除管道
cpp
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE);
if(m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
其实命名管道的代码比匿名管道代码实现要简单的多。作为一名程序员,你写的程序肯定是要报错的,有时候也会其他的情况,告警等等。那有没有一种方法可以知道我们的程序在哪里出错了,什么时间出错的?出错的原因是什么?并把这些信息写入一个文件中方便我们查看。有的 日志
日志
可变参数列表
关于打印日志需要用到可变参数列表那什么是可变参数列表?
可变参数列表是指函数在定义时允许接受不定数量的参数。在许多编程语言中,包括Python,Java和C++等,都支持可变参数列表的特性。
cpp
#include <stdarg.h>
#include <stdio.h>
// 一个接受可变参数列表的函数
void print_arguments(int num, ...) {
va_list args; // 定义一个va_list类型的变量
va_start(args, num); // 初始化args,指向第一个可变参数
for (int i = 0; i < num; i++) {
int value = va_arg(args, int); // 获取下一个可变参数的值
printf("%d ", value);
}
va_end(args); // 结束args的使用
printf("\n");
}
int main() {
print_arguments(3, 10, 20, 30);
// Output: 10 20 30
return 0;
}
获取时间的函数locatime
localtime
函数是C和C++标准库time.h
中的一个函数,用于将日历时间(秒数)转换为本地时间的struct tm
结构体。struct tm
结构体表示了时间的各个部分,如年、月、日、小时、分钟和秒等。
以下是localtime
函数的声明和简单示例:
struct tm *localtime(const time_t *timer);
timer
:指向要转换的日历时间的指针。
示例:
#include <time.h>
#include <stdio.h>
int main() {
time_t current_time;
struct tm *local_time;
time(¤t_time);
local_time = localtime(¤t_time);
printf("Current local time: %d-%d-%d %d:%d:%d\n",
local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
local_time->tm_hour, local_time->tm_min, local_time->tm_sec);
return 0;
}
snprintf
函数
snprintf
函数是C语言中的一个格式化字符串输出函数,它可以控制输出字符的数量,避免缓冲区溢出的风险。该函数的作用类似于printf
,但是它可以指定输出的最大长度。
以下是snprintf
函数的声明和简单示例:
int snprintf(char *str, size_t size, const char *format, ...);
str
:指向用于存储输出的字符数组的指针。size
:输出的字符数目(包括结尾的空字符)。format
:格式化字符串,用来指定输出的格式。...
:可变数量的参数,根据format
字符串的具体内容而定。示例:
#include <stdio.h>
int main() {
char buffer[50];
int num = 123;
snprintf(buffer, 50, "The number is %d\n", num);
printf("Output: %s", buffer);
return 0;
}
vsnprintf
函数
snprintf
函数将格式化后的字符串输出到buffer
数组中,最多输出50个字符。即使格式化后的字符串超过了50个字符,snprintf
也会在50个字符后停止输出,避免了缓冲区溢出的问题。通过使用
snprintf
可以更加安全地进行字符串格式化输出,特别是在处理用户输入等可能带有风险的数据时,可以有效防止缓冲区溢出漏洞。
vsnprintf
函数是C语言中的一个格式化字符串输出函数,它类似于snprintf
,允许用户指定输出字符的最大数量。与snprintf
不同的是,vsnprintf
函数使用一个va_list
类型的参数来传递可变数量的参数。
以下是vsnprintf
函数的声明和简单示例:
#include <stdio.h>
#include <stdarg.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
str
:指向用于存储输出的字符数组的指针。size
:输出的字符数目(包括结尾的空字符)。format
:格式化字符串,用来指定输出的格式。ap
:va_list
类型的参数列表。
示例:
#include <stdio.h>
#include <stdarg.h>
void format_string(char *buffer, size_t size, const char *format, ...) {
va_list args;
va_start(args, format);
vsnprintf(buffer, size, format, args);
va_end(args);
}
int main() {
char buffer[50];
format_string(buffer, 50, "The number is %d\n", 123);
printf("Output: %s", buffer);
return 0;
}
我们定义了一个辅助函数
format_string
,它使用vsnprintf
来格式化字符串并输出到buffer
数组中。通过使用va_list
类型的参数列表,vsnprintf
可以接受可变数量的参数,更加灵活地进行字符串格式化输出。
vsnprintf
函数通常用于创建可变参数的格式化字符串输出函数,可以在实现自定义格式化输出时使用,以提供更加灵活和安全的字符串处理能力。有了这些基础我们就可以写日志了
cpp
#pragma once
#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};