您的位置:首页 > 游戏 > 手游 > 商标购买平台_网站的搭建需要多少钱_新手怎么做网页_最新收录查询

商标购买平台_网站的搭建需要多少钱_新手怎么做网页_最新收录查询

2025/4/22 15:46:15 来源:https://blog.csdn.net/a_little_ji/article/details/146302051  浏览:    关键词:商标购买平台_网站的搭建需要多少钱_新手怎么做网页_最新收录查询
商标购买平台_网站的搭建需要多少钱_新手怎么做网页_最新收录查询

目录

  • 一、进程间通信介绍
    • 进程间通信的概念
    • 进程间通信的目的
    • 进程间通信的本质
    • 进程间通信的分类
  • 二、管道
    • 什么是管道
    • 匿名管道
      • 匿名管道的原理
      • pipe函数
      • 匿名管道使用步骤
      • 管道读写规则
      • 管道的特点
      • 管道的四种特殊情况
      • 管道的大小
    • 命名管道
      • 命名管道的原理
      • 使用命令创建命名管道
      • 创建一个命名管道
      • 命名管道的打开规则
      • 用命名管道实现server&client通信
      • 命名管道和匿名管道的区别
  • 三、实现一个简单日志函数
    • 可变参数
    • 日志三个部分:日志等级、日志时间、日志信息内容


一、进程间通信介绍

进程间通信的概念

进程间通信简称IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。

进程间通信的目的

  • 数据传输: 一个进程需要将它的数据发送给另一个进程。
  • 资源共享: 多个进程之间共享同样的资源。
  • 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件,比如进程终止时需要通知其父进程。
  • 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信的本质

进程间通信的本质就是让不同的进程看到同一份资源。

由于进程间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。

各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域。
在这里插入图片描述因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式。

进程访问这个资源的空间就是访问操作系统,进程代表用户,资源从创建、使用、释放都通过调用斯通调用接口,从底层到接口设计都要由操作系统独立设计,一般操作系统会有一个独立的通信模块——它属于文件系统——IPC通信模块,由于有好多通信的模块方式,所以定制了一个标准system V(本机内部)和posix(网络)。

进程间通信的分类

管道

  • 匿名管道
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

二、管道

什么是管道

管道是Unix中最古老的进程间通信的形式,我们把从一个进程连接到另一个进程的数据流称为一个“管道”。
例:查看当前使用云服务器上的登录用户个数
在这里插入图片描述其中,who命令和wc命令是两个系统级的命令,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
在这里插入图片描述

原理:

匿名管道

匿名管道的原理

匿名管道用于进程间通信,且仅限于本地父子进程之间的通信。

进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。

在这里插入图片描述注意:

  • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
  • 管道虽然用的是文件的方案,但操作系统不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率。这种文件是一批不会把数据写到磁盘当中的文件,也就是磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。

pipe函数

pipe函数用于创建匿名管道,pip函数的函数原型如下:

#include <unistd.h>				//头文件
int pipe(int pipefd[2]);

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

数组元素含义
pipefd[0]管道读端的文件描述符
pipefd[1]管道写端的文件描述符

pipe函数调用成功时返回0,调用失败时返回-1。

匿名管道使用步骤

在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
1、父进程调用pipe函数创建管道。在这里插入图片描述2、父进程创建子进程(管道的方式也会被继承)。

在这里插入图片描述
3、父进程关闭写端,子进程关闭读端(通常父子进程各持一个权限)。
在这里插入图片描述注意:

  1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。

以文件描述符的角度看待这三个步骤:

1、父进程调用pipe函数创建管道。
在这里插入图片描述2、父进程创建子进程。
在这里插入图片描述3、父进程关闭写端,子进程关闭读端。
在这里插入图片描述

管道读写规则

管道的特点

1、管道内部自带同步与互斥机制。

我们将一次只允许一个进程使用的资源,称为临界资源。管道在同一时刻只允许一个进程对其进行写入或是读取操作,因此管道也就是一种临界资源。

临界资源是需要被保护的,若是我们不对管道这种临界资源进行任何保护机制,那么就可能出现同一时刻有多个进程对同一管道进行操作的情况,进而导致同时读写、交叉读写以及读取到的数据不一致等问题。

为了避免这些问题,内核会对管道操作进行同步与互斥:

  • 同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。
  • 互斥: 一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源。

实际上,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。对于管道的场景来说,互斥就是两个进程不可以同时对管道进行操作,它们会相互排斥,必须等一个进程操作完毕,另一个才能操作,而同步也是指这两个不能同时对管道进行操作,但这两个进程必须要按照某种次序来对管道进行操作。

也就是说,互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,而同步的任务之间则有明确的顺序关系。

2、管道的生命周期随进程。

管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。

3、管道提供的是流式服务。

对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务:

  • 流式服务: 数据没有明确的分割,不分一定的报文段。
  • 数据报服务: 数据有明确的分割,拿数据按报文段拿。

4、管道是半双工通信的。

在数据通信中,数据在线路上的传送方式可以分为以下三种:

  1. 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  2. 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  3. 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道是半双工的,数据只能向一个方向流动,需要双方通信时,需要建立起两个管道。在这里插入图片描述

管道的四种特殊情况

  1. 写端进程不写,读端进程一直读,那么此时会因为管道里面没有数据可读,对应的读端进程会被挂起,直到管道里面有数据后,读端进程才会被唤醒。
  2. 读端进程不读,写端进程一直写,那么当管道被写满后,对应的写端进程会被挂起,直到管道当中的数据被读端进程读取后,写端进程才会被唤醒。
  3. 写端进程将数据写完后将写端关闭,那么读端进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。
  4. 读端进程将读端关闭,而写端进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。

其中前面两种情况就能够很好的说明,管道是自带同步与互斥机制的,读端进程和写端进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写端进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒。

第三种情况也很好理解,读端进程已经将管道当中的所有数据都读取出来了,而且此后也不会有写端再进行写入了,那么此时读端进程也就可以执行该进程的其他逻辑了,而不会被挂起。

第四种情况也不难理解,既然管道当中的数据已经没有进程会读取了,那么写端进程的写入将没有意义,因此操作系统直接将写端进程杀掉。而此时子进程代码都还没跑完就被终止了,属于异常退出,那么子进程必然收到了某种信号。

管道的大小

命名管道

命名管道的原理

匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间的通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相关进程之间的通信,可以使用命名管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

注意:

  1. 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
  2. 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。

使用命令创建命名管道

mkfifo命令创建一个命名管道

mkfifo fifo

创建一个命名管道

在程序中创建命名管道使用mkfifo函数,mkfifo函数的函数原型如下:

int mkfifo(const char *pathname, mode_t mode);

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。

  • 若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。
  • 若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。(注意当前路径的含义)

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。

命名管道的打开规则

1.如果当前打开操作是为读而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

2.如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

用命名管道实现server&client通信

实现服务端(server)和客户端(client)之间的通信之前,需要先让服务端运行起来,我们需要让服务端运行后创建一个命名管道文件,然后再以读的方式打开该命名管道文件,之后服务端就可以从该命名管道当中读取客户端发来的通信信息了。

命名管道和匿名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,由open函数打开。
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在于它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。

三、实现一个简单日志函数

补充前置知识可变参数就是如下的三个点

可变参数

可变参数至少有一个具体的参数,先简单演示一个加法函数。

 //可变参数,参数的个数是由用户控制的int sum(int n, ...)		//n代表参数的个数{va_list s;          	//这个其实就是char*,用该指针对可变参数逐一进行提取va_start(s,n);			//初始化s指针指向可变部分的起始地址nint sum=0;while(n)				//参数的个数{sum+=va_arg(s,int);          //第一个参数:s指针,第二个参数type,根据类型提参数(为了偏移量)n--;}va_end(s);       	   //置空,s=NULL     return sum;}//调用sum函数进行参数的相加int main(){//传入3个参数分别是2,5,8cout<<sum(3,2,5,8)<<endl;						//输出:15//传入5个参数分别是2,5,8,4,9cout<<sum(5,2,5,849)<<endl;					//输出:28	}return 0;

日志三个部分:日志等级、日志时间、日志信息内容

日志等级

显示是正常执行程序时发生的情况是正常还是报警等等。
info:常规消息
warning:报警消息
error:出错,可能需要立即处理
fatal:致命的错误
debug:调试

日志时间

认识几个库函数

#include <time.h>
time_t time(time_t *tloc);				//返回值是时间戳//该接口是将获取的时间戳转换成我们能看懂的形式,传入参数转换成struct tm形式
struct tm *localtime(const time_t *timep);//它的成员变量
struct tm 
{int tm_sec;    /* Seconds (0-60) */int tm_min;    /* Minutes (0-59) */int tm_hour;   /* Hours (0-23) */int tm_mday;   /* Day of the month (1-31) */int tm_mon;    /* Month (0-11) */int tm_year;   /* Year - 1900 */
};

具体实现

//1.调用库函数
#include <time.h>
time_t t=time(nullptr);							//返回值是时间戳
struct tm *localtime(const time_t *timep);		//转换时间戳为我们能看懂的形式
//简单打印的格式,我们常用到的年月日时分秒,注意年(1900)和月(0)的起始
void print()
{printf("%d-%d-%d %d:%d:%d\n",ctime->tm_year+1900,\ctime->tm_mon+1,ctime->tm_mday,ctime->tm_hour,ctime->tm_min,ctime->tm_sec);
}

这样我们就能实现基本的打印时间了
在这里插入图片描述

现在是光有日志时间然后然后再加上日志等级和日志内容,添加一下格式

  • 日志格式(默认部分+自定义部分)

我们要将时间信息打印在一个文件里而不是打印在显示器。
再继续认识一下库函数

//可变参数的个数是由用户决定的
#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);		//可变部分对参数进行处理//区别是转换了可变参数部分
#include <stdarg.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

日志格式的实现
先将日志的默认部分和自定义部分各自放入对应数组添加信息,最后再将这两数组合并放入新数组

#define SIZE 1024//日志等级函数,将整数风格转换成字符串风格,为了更方便看到打印的日志等级
std::string levelToString(int level)
{switch(level){case Info:return "Info";case Debug:return "Debug";case Warrning:return "Warrning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}
}//日志参数:等级+格式
void logmessage(int level,const char *format, ...)
{ //默认部分(时间+等级)time_t t=time(nullptr);             //返回值是时间戳struct tm* ctime=localtime(&t);char leftbuf[SIZE];                 //存放默认部分//年份是按1900年开始,月份第0月,将这些信息放入leftbuf数组里,注意转换字符串的函数snprintf(leftbuf,sizeof(leftbuf),"[%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 rightbuf[SIZE];                //存放自定义部分vsnprintf(rightbuf,sizeof(rightbuf),format,s);va_end(s);//存放日志的所有内容(默认部分+自定义部分)char logtxt[SIZE*2];                //将日志内容放入logtxt数组里snprintf(logtxt,sizeof(logtxt),"%s %s\n",leftbuf,rightbuf);printf("%s",logtxt);                //将日志内容都打印到显示器
}

最后整理一下,添加了一些打印在文件的函数,把他们放入一个类里。
日志完整代码

#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//日志等级0~4
#define Info 0
#define Debug 1
#define Warrning 2
#define Error 3
#define Fatal 4#define Screan 1                   //向显示器打印
#define Onefile 2                  //向一个文件打印
#define Classfile 3                //分类打印#define logFile "log.txt"class Log
{
public:Log()          {printMethod = Screan;       //默认向显示器打印path="./log/";}void Enable(int method)         //打印格式{printMethod = method;}//日志等级函数,将整数风格转换成字符串风格std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warrning:return "Warrning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}/*//传入日志等级和内容void logmessage(int level, const char* format, ...){//默认部分(时间+等级)time_t t = time(nullptr);             //返回值是时间戳struct tm* ctime = localtime(&t);char leftbuf[SIZE];                 //存放默认部分//年份是按1900年开始,月份第0月,将这些信息放入leftbuf数组里,注意转换字符串的函数snprintf(leftbuf, sizeof(leftbuf), "[%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 rightbuf[SIZE];                //存放自定义部分vsnprintf(rightbuf, sizeof(rightbuf), format, s);va_end(s);//存放日志的所有内容(默认部分+自定义部分)char logtxt[SIZE * 2];//将日志内容放入logtxt数组里snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuf, rightbuf);//printf("%s", logtxt);                //将日志内容都打印到显示器printLog(level,logtxt);}*/void printLog(int level,const std::string logtxt){switch(printMethod){case Screan: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.txtif(fd<0) return; write(fd,logtxt.c_str(),logtxt.size());close(fd);  }void printClassFile(int level,const std::string& logtxt){std::string filename=logFile;		//默认文件名logFilefilename+=".";filename+=levelToString(level);		//日志后缀,例如:log.txt.Debug/warningprintOneFile(filename,logtxt);}~Log(){}//重载后直接就能调用log了void operator()(int level,const cahr *format,...)		{time_t t = time(nullptr);             //返回值是时间戳struct tm* ctime = localtime(&t);char leftbuf[SIZE];                 //存放默认部分//年份是按1900年开始,月份第0月,将这些信息放入leftbuf数组里,注意转换字符串的函数snprintf(leftbuf, sizeof(leftbuf), "[%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 rightbuf[SIZE];                //存放自定义部分vsnprintf(rightbuf, sizeof(rightbuf), format, s);va_end(s);//存放日志的所有内容(默认部分+自定义部分)char logtxt[SIZE * 2];//将日志内容放入logtxt数组里snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuf, rightbuf);//printf("%s", logtxt);                //将日志内容都打印到显示器printLog(level,logtxt);}
private:int printMethod;		//向显示器或文件打印的格式std::string path;		//路径
};

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com