Linux命名管道与共享内存
命名管道与共享内存
命名管道介绍和基本使用
理解了匿名管道后,命名管道的理解就会变得容易。在前面使用匿名管道时可以发现,之所以可以匿名是因为由父进程创建,子进程拷贝所以子进程和父进程都可以看到这个管道。但是如果对于任意两个进程,因为进程之间是独立的,需要任意两个进程看到这个管道就需要借助进程通信,但是匿名管道本身就是用于进程通信,所以匿名管道无法用于任意的两个进程。对此,根据一个文件可以被任意一个进程打开并由任意多个进程共享,如果设计一个文件作为两个进程通信方式就可以解决这个问题,此时这个文件也被称为命名管道
在Linux中,创建命名管道的方式有两种:
- 终端命令
mkfifo 文件名 - 函数调用:
int mkfifo(const char *pathname, mode_t mode)
首先介绍终端命令,使用mkfifo命令创建一个管道文件,这个文件的类型是p,表示管道(pipe)类型的文件,在当前路径下创建一个命名管道如下图所示:

当使用一个指令向管道内写入数据,再使用另外一个指令从管道中读取数据,就可以看到下面的效果:

在匿名管道部分提到过,在终端中指向的指令实际上是一个进程,所以此时使用echo的进程向命名管道中写入数据,使用cat的进程从命名管道中读取数据,此时就是进程间通信
可以看到,如果两个进程要使用命名管道进行通信,就必须有一个进程先创建命名管道,另外一个进程获取命名管道,所以两个进程使用命名管道的方式为:
- 创建+使用
- 获取+使用
如果想要删除一个文件,就可以使用前面提到的unlink命令删除管道文件,也可以使用rm删除
命名管道的原理
之所以叫命名管道,本质就是因为命名管道就是一个文件,一个文件就存在自己的路径,在Linux中,要查找一个文件就会根据这个文件的路径进行查找,此时查出的结果一定是唯一的,所以任意两个进程要通过命名管道进行通信就必须通过路径打开命名管道,也就是打开文件,此时二者就构成了访问同一份资源的通信条件
既然命名管道是一个文件,那么在磁盘上一定有其对应的inode编号与文件名映射,那么是否可以直接使用一个普通文件完成进程通信?实际上也是可以的,但是对于普通文件来说,其存在最大的问题就是会将文件中的内容刷新到磁盘上,而对于命名管道来说,之所以单独为他创建一个文件类型,就是因为他不进行内容刷新,完全是内存级别的文件,所以其在磁盘上的inode编号和文件名映射也只是占个位置
使用函数调用完成两个进程通信
创建命名管道的函数调用为int mkfifo(const char *pathname, mode_t mode),其第一个参数传递路径名称,表示在哪个目录下创建命名管道(可以传递命名管道的名称),第二个参数传递命名管道的权限,其与文件权限一样。如果命名管道创建成功函数返回0,否则返回-1
前面已经介绍过使用命令如何创建命名管道,接下来主要介绍如何使用函数调用创建命名管道,基本上分为下面的步骤:
- 第一个进程创建命名管道并打开管道进行使用
- 第二个进程获取(打开)对应的命名管道并进行使用
所以此处需要用到两个可执行程序,首先创建对应的Makefile:
SERVER=Server
CLINET=Client
SERVER_CC=Server.cc
CLINET_CC=Client.cc
CC=g++.PHONY: all
all: $(SERVER) $(CLINET)$(SERVER): $(SERVER_CC)$(CC) -o $(SERVER) $(SERVER).cc -std=c++11$(CLINET): $(CLINET_CC)$(CC) -o $(CLINET) $(CLINET).cc -std=c++11.PHONY: clean
clean:rm -f $(SERVER) $(CLINET)
在上面的Makefile中,为了同时生成出两个可执行程序,需要用到all,其依赖关系为两个可执行程序,但是因为这两个可执行程序还不存在,Makefile会向下执行直到all的依赖全部存在为止。另外,Server表示创建命名管道的一方,Client表示使用获取命名管道的一方,本次演示Client向命名管道中写入,Server从命名管道中读取
在匿名管道部分实现了简单的进程池,当时也处理了从面向过程转向面向对象,所以本次直接使用面向对象的思路进行设计
在下面的两个类的设计中,有些内容是共用的,所以放在单独的一个头文件中:
#ifndef __SHARED_HPP__
#define __SHARED_HPP__#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>// 定义路径
// 在当前进程的CWD下创建命名管道fifo
const std::string pipe_path = "./fifo";
// 指定命名管道的权限
const mode_t pipe_mode = 0600;
// 缓冲区大小
const int buffer_size = 1024;#endif
设计Server类
因为要创建命名管道,所以考虑在Server类对象创建时就创建命名管道。注意,因为命名管道是一个文件,有文件就会有对应的权限,所以命名管道也有对应的权限,也就是说一个进程创建的管道文件,其他进程需要向该管道读取或者从该管道输入都要有对应的权限,所以在创建管道时也需要给予管道文件的权限:
class Server
{
public:Server(){// 创建命名管道int ret = mkfifo(pipe_path.c_str(), pipe_mode);if (ret < 0){std::cout << "管道创建失败" << std::endl;return;}}private:
};
接着,因为Server需要从命名管道中读取,所以可以考虑实现一个函数用于打开对应的命名管道文件,因为打开文件会返回对应的文件描述符,所以可以考虑添加一个成员_fd存储命名管道的文件描述符
// 打开管道文件
void openFifo()
{_fd = open(pipe_path.c_str(), pipe_mode);// 错误处理if (_fd < 0){std::cout << "打开管道失败" << std::endl;return;}
}
在上面的代码中,_fd就是成员变量,用于存储管道的文件描述符
因为Server是读取数据,所以考虑在Server类中提供读取方法,该方法返回读取到的字节数,如果为0,说明读取到文件结尾,可能是写端关闭,否则就是正常读取到的数据,本次以读取字符串为例,为了保证外部可以直到读取到的字符串,需要调用方传递一个实参,此时函数的形参应该应该作为输出型参数,下面有常见的三种写法分别代表不同类型的参数:
*表示输出型参数const &表示输入型参数&表示输入输出型参数
// 从管道中读取数据
int readFromPipe(std::string *out)
{char buffer[buffer_size] = {0};int ret = read(_fd, buffer, buffer_size);if (ret > 0)*out = buffer; // 通过输出型参数带离函数return ret;
}
最后就是关闭管道,关闭管道只需要关闭对应的文件并删除命名管道文件即可,在代码中删除命名管道文件可以使用unlink系统调用:
int unlink(const char *pathname);
提供对应的函数如下:
// 关闭管道
void closePipe()
{if (_fd > 0)close(_fd);// 删除命名管道int ret = unlink(pipe_path.c_str());if (ret < 0){std::cout << "删除命名管道失败" << std::endl;return;}
}
设计Client类
设计Client类的思路和Server类的思路非常类似,只需要将Server类中的「创建管道」改为「获取(打开)管道」,将「管道读取」改为「管道写入」,代码整体如下:
class Client
{
public:Client(){// 打开管道_fd = open(pipe_path.c_str(), O_WRONLY);if (_fd < 0){std::cout << "管道打开失败" << std::endl;return;}}// 写入void writeToPipe(){const std::string str = "Hello Pipe";write(_fd, str.c_str(), str.size());}// 关闭管道void closePipe(){if (_fd > 0)close(_fd);}private:int _fd;
};
至此,命名管道的用法就是上面的过程,上面两个类还可以对相同的代码进行简化,此处就不再赘述,下面是对应的主函数:
=== “Server.cc”
#include <iostream>#include "Server.hpp"using namespace std;int main(){// 创建Server类对象Server sv;sv.openFifo();while (true){string out;int ret = sv.readFromPipe(&out);if (ret > 0)std::cout << out << std::endl;else if (ret == 0) // 读到文件结尾,可能是写端关闭,结束读取break;}// 关闭管道sv.closePipe();return 0;}
=== “Client.cc”
#include <iostream>#include "Client.hpp"using namespace std;int main(){// 创建Client类对象Client cl;while (true){cl.writeToPipe();sleep(1);}return 0;}
运行结果如下:

共享内存介绍
除了前面提到的两个管道可以进行进程通信外,共享内存也是一种方式,但是共享内存是System V标准下的进程间通信方式。共享内存本质就是在内存上开辟一块空间并将其链接到两个进程的PCB中,从而让两个进程都能看到同一块资源,进而实现进程间通信,具体原理如下图所示:

在上面的原理图中,将共享内存通过页表链接到进程PCB的过程叫做挂接,当进程PCB与共享内存断开连接的过程叫做去关联
因为共享内存在操作系统中可以存在多个,所以操作系统也需要对开辟的共享内存进行管理,而两个进程要想确定找到的是同一个共享内存就必须通过唯一的标识符,所以共享内存就是通过实际的物理内存块+内核数据结构组成
共享内存有如下的特点:
- 通信速度最快,因为其不需要调用I/O接口,从而不需要内核级文件缓冲区,减少了通信内容的拷贝次数
- 共享内存没有任何保护机制,导致其不会出现一个进程正在写,另一个进程正在阻塞的现象,所以更加容易出现数据不一致的问题,这也就意味着需要通过加锁以及同步的方式对共享内存进行保护
共享内存的基本使用
使用共享内存与前面使用管道的思路是大致一致的,尤其与命名管道非常类似,同样需要一方先向内存中申请共享内存并使用,另一方再获取到共享内存并使用,所以同样需要两个可执行文件
基本步骤如下:
- 第一个进程申请共享内存并挂接进行使用
- 第二个进程获取对应的共享内存并挂接进行使用
首先创建对应的Makefile:
SERVER=Server
CLINET=Client
SERVER_CC=Server.cc
CLINET_CC=Client.cc
CC=g++.PHONY: all
all: $(SERVER) $(CLINET)$(SERVER): $(SERVER_CC)$(CC) -o $(SERVER) $(SERVER).cc -std=c++11$(CLINET): $(CLINET_CC)$(CC) -o $(CLINET) $(CLINET).cc -std=c++11.PHONY: clean
clean:rm -f $(SERVER) $(CLINET)
设计Server类
本次实现时与命名管道类似,让Server类申请共享内存,并从共享内存中读取数据,因为共享内存是由操作系统开辟的,所以进程只能向操作系统申请,此时就需要用到系统调用接口shmget:
int shmget(key_t key, size_t size, int shmflg);
对于shmget函数来说,第一个参数表示共享内存的标识符,这个标识符由用户指定,但是一般情况下用户只需要调用ftok函数即可获取到对应的key_t值,ftok函数如下:
key_t ftok(const char *pathname, int proj_id);
该函数传入两个参数,第一个参数表示路径,第二个参数表示项目ID,这两个参数没有固定的内容,但是一般使用有意义的路径和项目ID,该函数返回一个key_t的值
shmget函数的第二个参数传递共享内存需要开辟的空间大小,因为操作系统每一次读取是按照4kb进行,所以一般建议size的值为4096的整数倍
需要注意的是,如果需要开辟的共享内存大小不足4096的整数倍,操作系统会开辟刚好大于需求的4096整数倍的共享内存,但是实际给使用方就只有需要的开辟大小
shmget第三个参数为标记位,一般常用的有两个标记:
IPC_CREAT:如果单独使用IPC_CREAT,那么就代表如果指定的共享内存不存在就创建,否则就使用已有的共享内存IPC_EXCL:单独使用无意义,但是一般配合IPC_CREAT可以实现当指定的共享内存不存在时就创建,否则就报错
这两个标记一起使用的方式与open函数中的打开模式一样,只需要按位或即可
shmget函数申请成功会返回共享内存标识符,否则返回-1。注意,这个标识符并不是前面传入的key_t的值,而是类似于数组下标的一个值,从0开始。后面使用的与共享内存相关的大部分操作都会使用共享内存标识符而不是key_t值(尽管可以使用key_t值,但是不推荐),例如管理共享内存的指令ipcs -m和ipcrm -m 共享内存标识符,其中ipcs -m指令是查看当前用户创建的共享内存的个数,ipcrm -m 共享内存标识符表示根据共享内存标识符释放对应的共享内存
需要注意,共享内存与前面的两个管道不同,其生命周期跟随操作系统,而不是跟随进程,所以进程退出不会自动销毁共享内存,需要在代码层面或者指令关闭共享内存
所以,共享内存标识符和key_t值的关系为:共享内存标识符是给用户使用的一个标识共享内存的标识符,便于用户更好的去管理共享内存,而key_t值是提供给操作系统使用,用于区分不同的共享内存
同样,一些相同的内容可以放在一个公共的头文件中方便调用:
#ifndef __SHARED_HPP__
#define __SHARED_HPP__#include <iostream>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/ipc.h>// 项目路径
const std::string pathName = "./";
// 项目ID
const int proj_id = 25;
// 共享内存大小,4kb
int shm_size = 4096;
// 共享内存权限
mode_t shm_mode = 0600;
#endif
有了上面的内容后,就可以开始设计Server类,首先是申请共享内存,调用ftok函数获取key_t值,再通过该值申请共享内存,考虑在Server对象创建时自动创建共享内存:
class Server
{
public:Server(){// 调用ftok函数获取key_t值key_t key = ftok(pathName.c_str(), proj_id);// 根据key申请共享内存// 申请时需要确保不存在创建,存在就报错,确保获取到的是最新的共享内存_shmid = shmget(key, shm_size, IPC_CREAT | IPC_EXCL);if (_shmid < 0){std::cout << "共享内存申请失败" << std::endl;return;}std::cout << "共享内存申请成功" << std::endl;}private:int _shmid; // 共享内存标识符
};
如果此时创建Server对象并执行对应的可执行程序就可以判断是否成功创建共享内存,如下图所示:

创建完成共享内存后,就需要考虑将共享内存挂接到指定的进程中,所以可以使用shmat接口:
void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
该接口虽然有三个参数,但是最后一个参数和第二个参数暂时用不到,只需要传递0和NULL即可,第一个参数就是共享内存唯一标识符。该接口返回一个void *代表共享内存的起始地址,既然是void *证明可以使用共享内存传递任何内容,如果挂接失败,该函数会返回void *类型的-1
根据上面的描述,下面实现一个函数用于挂接,为了后面可以向调用层返回共享内存的起始地址,考虑使用一个成员变量接收shmat的返回值:
void Connect()
{_shm_address = shmat(_shmid, NULL, 0);// 注意不要强制转换为int,可能会因为精度丢失导致报错if ((long long)_shm_address < 0){std::cout << "挂接失败" << std::endl;return;}std::cout << "挂接成功" << std::endl;
}
同样,为了测试挂接是否成功,可以创建Server对象,调用挂接方法,如果使用ipcs -m看到连接数(nattach)不为0,说明挂接成功,需要注意,为了防止进程在查看挂接前退出,可以使用sleep接口:

根据打印的结果可以判断挂接已经失败,对应的连接数也为0:

之所以挂接失败,本质上是因为在申请共享内存是并没有读写权限,这也就是为什么会有shm_mode的原因,解决方案也很简单,只需要在申请共享内存时在shmflg参数部分通过按位或添加读写权限即可:
class Server
{
public:Server(){// ..._shmid = shmget(key, shm_size, IPC_CREAT | IPC_EXCL | shm_mode);// ...}private:int _shmid; // 共享内存标识符
};
此时编译运行即可看到挂接成功:

对应的连接数从0变为1:

完成了挂接之后,接下来就可以让Server从共享内存中读取数据了,但是因为共享内存是挂接到进程上而不是在其他位置,所以不需要对应的接口,进程只需要从自己的PCB空间中找到共享内存读取即可,所以为了方便处理,本次以读取字符串为例,并将读取过程设计在Server主函数中,Server类只需要返回挂接的共享内存起始地址即可:
=== “Server.hpp”
void *getAddress(){return _shm_address;}
=== “Server.cc”
// 从共享内存中读取char *shm_mem = (char *)sv.getAddress();while (true){// 读取内容的方式与读取堆上的内容相似printf("%s\n", shm_mem);}
读取完毕后就是去关联,同样,去关联可以使用对应的接口shmdt:
int shmdt(const void *shmaddr);
该函数参数只需要传递共享内存的起始地址即可,函数返回0代表去关联成功,否则失败,实现对应的接口如下:
void disconnect()
{int ret = shmdt(_shm_address);if (ret < 0){std::cout << "去关联失败" << std::endl;return;}std::cout << "去关联成功" << std::endl;
}
去关联结束后,就需要释放共享内存空间,防止内存泄漏,可以使用shmctl接口:
int shmctl(int shmid, int op, struct shmid_ds *buf);
对于该接口来说,虽然有三个参数,但是实际上只需要使用前两个参数,第三个参数直接填入NULL即可,第一个参数代表共享内存标识符,第二个参数代表一个操作标记,常用的标记为IPC_RMID,表示标记共享内存段将被释放,利用该接口可以在Server类中实现,考虑到自动调用,可以考虑使用Server对象的析构函数:
~Server()
{// 释放共享内存shmctl(_shmid, IPC_RMID, NULL);std::cout << "释放共享内存" << std::endl;
}
至此,Server类就设计完毕了,接下里就是考虑设计Client类
设计Client类
Client类用于向共享内存中写入数据,所以依旧还是需要先获取到对应的共享内存,并将对应的共享内存起始地址挂接到Client进程的PCB上,考虑在Client初始化对象时就获取共享内存,所以获取的步骤可以写在Client类的构造函数中。对于获取共享内存来说,需要保证「存在时获取」,所以只需要使用IPC_CREAT即可。同样,为了保存对应的共享内存标识符,可以使用一个成员变量接收shmget的返回值:
class Client
{
public:Client(){key_t key = ftok(pathName.c_str(), proj_id);_shmid = shmget(key, shm_size, IPC_CREAT);if (_shmid < 0){std::cout << " 获取共享内存失败" << std::endl;return;}std::cout << "获取共享内存成功" << std::endl;}private:int _shmid; // 共享内存的唯一标识符
};
接着处理挂接,思路与Server类一致:
void connect()
{_shm_address = shmat(_shmid, NULL, 0);if ((long long)_shm_address < 0){std::cout << "共享内存挂接失败" << std::endl;return;}std::cout << "共享内存挂接成功" << std::endl;
}
在Client的主函数中创建Client对象,为了可以看到共享内存的链接数,可以添加sleep,编译运行后先运行Server再运行Client结果如下:

与Server一样,Client向上层返回共享内存的起始地址,上层只需要向该空间写入内容即可被Server端读取:
=== “Client.hpp”
void *getAddress()
{return _shm_address;
}
=== “Client.cc”
char *str = (char *)cl.getAddress();
while (true)
{scanf("%s", str);
}
最后就是处理Client去关联,但是Client不需要处理释放共享内存,因为Server端已经进行了处理,为了保证Client一定可以断开连接,考虑单独写一个接口而不是放在Client的析构函数中:
void disconnect()
{int ret = shmdt(_shm_address);if (ret < 0){std::cout << "去关联失败" << std::endl;return;}std::cout << "去关联成功" << std::endl;
}
最后,完善两个程序的主函数如下:
=== “Server.cc”
#include <iostream>#include "Server.hpp"int main(){// 创建Server对象Server sv;sv.Connect();// 从共享内存中读取char *shm_mem = (char *)sv.getAddress();int num = 0;while (num++ <= 30){// 读取内容的方式与读取堆上的内容相似printf("%s\n", shm_mem);sleep(1);}sv.disconnect();return 0;}
=== “Client.cc”
#include <iostream>#include "Client.hpp"int main(){Client cl;cl.connect();char *str = (char *)cl.getAddress();char ch = 'a';while (ch <= 'g'){str[ch - 'a'] = ch;ch++;sleep(1);}cl.disconnect();return 0;}
编译运行代码,先运行Server,再运行Client,在Client运行的窗口中输入内容就可以在Server端看到输出,为了保证输出的效果,可以使用sleep:
Client输入:

Server输出:

从上面Server的输出结果可以看出,尽管Client只输入了两次内容,但是Server端在等待输入前会一直打印空,在第二次输入之前,会一直打印第一次输入的内容,根据这个特点也就可以看出共享内存没有阻塞等待的特点,这就可能会导致输入的内容和输出的内容不一致的情况
上面的代码中存在一些共性的地方,比如创建或者获取共享内存时的标记可以通过参数传递、去连接的代码和获取共享内存的代码等,可以进行抽离
结合命名管道保护共享内存
因为共享内存没有任何保护机制,所以在使用过程中为了防止出现数据不一致的问题,需要对共享内存进行使用保护,常见的保护是进行加锁和同步,但是因为目前还没有提到锁机制,所以暂时用命名管道替代
本次使用命名管道保护共享内存的思路如下:
- 写端向共享内存中写入数据,写完后向命名管道中写入数据(相当于通知读端可以读取共享内存)
- 读端读取完命名管道的内容后就会读取共享内存中的数据,否则就会一直阻塞在命名管道
示意图如下:

上面的做法只能保证写端写入时不会被读端读取到不完整的内容,但是会存在读端读取时写端还在写的情况,所以理论上来说读端和写端都需要两个命名管道对操作共享内存的代码进行包裹,本次只演示上面提到的情况
完善Server类
因为Server类需要向命名管道中读取,所以首先Server类除了需要申请共享空间外,还需要创建命名管道,并且提供向命名管道中读取数据的接口,使用代码与前面命名管道的代码是一致的,细节不在赘述:
本次规定向命名管道中写入一个整数代表信号
=== “Server.hpp”
class Server{public:Server(){// ...// 创建命名管道int ret = mkfifo(pipe_path.c_str(), pipe_mode);if (ret < 0){std::cout << "命名管道创建失败" << std::endl;return;}std::cout << "命名管道创建成功" << std::endl;}// ...void openPipe(){_fd = open(pipe_path.c_str(), O_RDONLY);}ssize_t readFromPipe(int *out){ssize_t n = read(_fd, &(*out), sizeof(int));return n;}void closePipe(){close(_fd);// 删除命名管道int ret = unlink(pipe_path.c_str());if (ret < 0){std::cout << "删除命名管道失败" << std::endl;return;}std::cout << "删除命名管道成功" << std::endl;}// ...~Server(){// ...// 关闭管道closePipe();}private:int _shmid; // 共享内存标识符void *_shm_address; // 共享内存起始位置int _fd; // 命名管道文件描述符};
=== “Server.cc”
#include <iostream>
#include "Server.hpp"int main()
{// 创建Server对象Server sv;sv.Connect();sv.openPipe();// 从共享内存中读取char *shm_mem = (char *)sv.getAddress();int num = 0;while (num++ <= 30){int sig = 0;// 等待读取命名管道ssize_t n = sv.readFromPipe(&sig);if (n == sizeof(int)) // 成功从命名管道读取到数据时说明读到信号,可以读取共享内存{// 读取内容的方式与读取堆上的内容相似printf("%s\n", shm_mem);}else if (n == 0) // 写端关闭,停止读取{break;}}sv.disconnect();return 0;
}
完善Client类
因为Client是写入端,所以需要通知Server可以开始进行读取,即需要向命名管道中写入数据代表信号,因为是确保读取端在写入端写完后读取,所以需要再写入共享内存的步骤结束后向命名管道中写入,具体代码如下:
=== “Client.hpp”
class Client
{
public:Client(){// ...// 打开命名管道_fd = open(pipe_path.c_str(), O_WRONLY);if (_fd < 0){std::cout << "命名管道打开失败" << std::endl;return;}std::cout << "命名管道打开成功" << std::endl;}// ...void writeToPipe(){int sig = 1;write(_fd, &sig, sizeof(int));}void closePipe(){close(_fd);}// ...private:// ...int _fd; // 命名管道文件描述符
};
=== “Client.cc”
#include <iostream>
#include "Client.hpp"int main()
{Client cl;cl.connect();char *str = (char *)cl.getAddress();char ch = 'a';while (ch <= 'n'){str[ch - 'a'] = ch;ch++;// 向命名管道中写入表示通知写端可以读取cl.writeToPipe();sleep(1);}cl.closePipe();cl.disconnect();return 0;
}
编译运行上面的代码,就可以看到不会出现某一条相同的内容被共享内存读取端打印多次了:

本节彩蛋(获取时间的接口)
在Linux中,如果想通过代码看到当前的日期和时间可以使用localtime接口,这个接口的作用是根据指定的时间戳转换为日期和时间,该接口如下:
struct tm *localtime(const time_t *timep);
该接口可以传递一个参数,表示获取当前系统时间的时间戳,可以使用time函数获取:
time_t time(time_t *_Nullable tloc);
localtime返回一个struct tm的结构体指针,而struct tm结构体原型如下:
struct tm
{int tm_sec; /* Seconds [0, 60] */int tm_min; /* Minutes [0, 59] */int tm_hour; /* Hour [0, 23] */int tm_mday; /* Day of the month [1, 31] */int tm_mon; /* Month [0, 11] (January = 0) */int tm_year; /* Year minus 1900 */int tm_wday; /* Day of the week [0, 6] (Sunday = 0) */int tm_yday; /* Day of the year [0, 365] (Jan/01 = 0) */int tm_isdst; /* Daylight savings flag */long tm_gmtoff; /* Seconds East of UTC */const char *tm_zone; /* Timezone abbreviation */
};
所以此时就可以写出下面获取时间的代码:
#include <iostream>
#include <string>
#include <ctime>std::string GetCurrTime()
{time_t t = time(nullptr);struct tm *curr = ::localtime(&t);char currtime[32];snprintf(currtime, sizeof(currtime), "%d-%d-%d %d:%d:%d",curr->tm_year + 1900,curr->tm_mon + 1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);return currtime;
}
相关文章:
Linux命名管道与共享内存
命名管道与共享内存 命名管道介绍和基本使用 理解了匿名管道后,命名管道的理解就会变得容易。在前面使用匿名管道时可以发现,之所以可以匿名是因为由父进程创建,子进程拷贝所以子进程和父进程都可以看到这个管道。但是如果对于任意两个进程…...
maven web项目如何定义filter
在 Maven Web 项目中定义一个 Servlet 过滤器(Filter),需要遵循 Java Servlet 规范,并利用 Maven 来管理项目结构和依赖。下面是如何在 Maven Web 项目中定义和配置一个过滤器的基本步骤: 1. 创建过滤器类 首先&…...
使用 Notepad++ 编辑显示 MarkDown
Notepad 是一款免费的开源文本编辑器,专为 Windows 用户设计。它是替代记事本(Notepad)的最佳选择之一,因为它功能强大且轻量级。Notepad 支持多种编程语言和文件格式,并可以通过插件扩展其功能。 Notepad 是一款功能…...
@synchronized的使用
synchronized 介绍 synchronized 是 Objective-C 提供的一种 互斥锁(Mutex),它用于确保一段代码在同一时间只有一个线程能执行,避免多线程访问共享资源时出现数据竞争。 基本语法 synchronized (lockObject) {// 需要加锁的代码…...
解锁Rust:融合多语言特性的编程利器
如果你曾为理解Rust的特性或它们之间的协同工作原理而苦恼,那么这篇文章正是为你准备的。 Rust拥有许多令人惊叹的特性,但这些特性并非Rust所独有。实际上,Rust巧妙地借鉴了众多其他语言的优秀特性,并将它们融合成了一个完美的整体。深入了解Rust这些重要特性的来源以及它是…...
zyNo.23
SQL注入漏洞 1.SQL语句基础知识 一个数据库由多个表空间组成,sql注入关系到关系型数据库,常见的关系型数据库有MySQL,Postgres,SQLServer,Oracle等 以Mysql为例,输入 mysql-u用户名-p密码 即可登录到MySQL交互式命令行界面。 既然是…...
visual studio 在kylin v10上跨平台编译时c++标准库提示缺少无法打开的问题解决
情况1:提示无法打开 源文件 "string"之类导致无法编译 情况2:能编译,但无法打开这些库文件或标准库使用提示下划红色问题 解决方案: 一、通过工具->选项->跨平台里,在“远程标头IntelliSense管理器”更新下载一下…...
黑马Mistral Le chat逆转deepseek
法国人工智能聊天机器人出来了。 Mistral AI比deepseek 性能快很多,准确率更高,非常好用。 全新的发现! 站在老美已经出来的方法&理论上,感觉有0.2亿美金和有gpu算力,感觉搞一个超越国内deepseek难道其实…...
Spring Cloud — 深入了解Eureka、Ribbon及Feign
Eureka 负责服务注册与发现;Ribbon负责负载均衡;Feign简化了Web服务客户端调用方式。这三个组件可以协同工作,共同构建稳定、高效的微服务架构。 1 Eureka 分布式系统的CAP定理: 一致性(Consistency)&am…...
Web项目测试专题(六)压力测试
概述: 压力测试检验Web应用在高并发、高负载情况下的表现,帮助预估系统承载能力和发现瓶颈 步骤: 并发用户测试:增加虚拟用户数测试系统在多人同时使用时的表现 负载测试:模拟高负载情况测试系统的稳定性和响应时间…...
2.5 使用注解进行单元测试详解
Mockito 使用注解进行单元测试详解 Mockito 提供了一系列注解来简化测试代码的编写,减少手动创建和管理 Mock 对象的样板代码。结合 JUnit 5,可以更高效地构建清晰、易维护的单元测试。 1. 核心注解概览 注解作用Mock创建并注入一个 Mock 对象…...
2025年SEO工具有哪些?老品牌SEO工具有哪些
随着2025年互联网的发展和企业线上营销的日益重要,SEO(搜索引擎优化)逐渐成为了提高网站曝光率和流量的重要手段。SEO的工作不仅仅是简单地通过关键词优化和内容发布就能够实现的,它需要依赖一系列专业的SEO工具来帮助分析、监测和…...
使用 React 16+Webpack 和 pdfjs-dist 或 react-pdf 实现 PDF 文件显示、定位和高亮
写在前面 在本文中,我们将探讨如何使用 React 16Webpack 和 pdfjs-dist 或 react-pdf 库来实现 PDF 文件的显示、定位和高亮功能。这些库提供了强大的工具和 API,使得在 Web 应用中处理 PDF 文件变得更加容易。 项目设置 首先,我们需要创建…...
LabVIEW显微镜成像偏差校准
在高精度显微镜成像中,用户常常需要通过点击图像的不同位置,让电机驱动探针移动到指定点进行观察。然而,在实际操作中,经常会遇到一个问题:当点击位于图像中心附近的点时,探针能够相对准确地定位࿱…...
【Elasticsearch】文本分析Text analysis概述
文本分析概述 文本分析使 Elasticsearch 能够执行全文搜索,搜索结果会返回所有相关的结果,而不仅仅是完全匹配的结果。 如果你搜索“Quick fox jumps”,你可能希望找到包含“A quick brown fox jumps over the lazy dog”的文档,…...
23页PDF | 国标《GB/T 44109-2024 信息技术 大数据 数据治理实施指南 》发布
一、前言 《信息技术 大数据 数据治理实施指南》是中国国家标准化管理委员会发布的关于大数据环境下数据治理实施的指导性文件,旨在为组织开展数据治理工作提供系统性的方法和框架。报告详细阐述了数据治理的实施过程,包括规划、执行、评价和改进四个阶…...
AI代码生成器如何重塑前端开发的工作环境
近年来,人工智能(AI)技术迅猛发展,深刻地改变着各行各业的工作方式。在软件开发领域,AI写代码工具的出现更是掀起了一场革命,尤其对前端开发工程师的工作环境和协作方式产生了深远的影响。本文将深入探讨AI…...
kafka的架构和工作原理
目录 Kafka 架构 Kafka 工作原理 Kafka 数据流 Kafka 核心特性 总结 Kafka 架构 1. 生产者(Producer) 2. 消费者(Consumer) 3. 主题(Topic) 4. 分区(Partition) 5. 副本(Replica) 6. 代理(Broker) 7. ZooKeeper(旧版本)/KRaft(新版本) Kafka 工作…...
Xcode证书密钥导入
证书干嘛用 渠道定期会给xcode证书,用来给ios打包用,证书里面有记录哪些设备可以打包进去。 怎么换证书 先更新密钥 在钥匙串访问中,选择系统。(选登录也行,反正两个都要导入就是了)。 mac中双击所有 .p12 后缀的密钥ÿ…...
索引的详细介绍
数据库索引是一种用于加速数据检索的数据结构,类似于书籍的目录。通过索引,数据库可以快速定位数据,而无需扫描整个表。以下是关于数据库索引的详细介绍: 1. 索引的基本概念 定义:索引是数据库表中一列或多列的值及其…...
Python 基于 OpenCV 的人脸识别上课考勤系统(附源码,部署教程)
博主介绍:✌2013crazy、10年大厂程序员经历。全网粉丝12W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇&a…...
TikTok网页版访问受限?一文解析解决方案
TikTok网页版是许多用户用来浏览视频、管理账号和发布内容的重要工具。然而,部分用户可能会遇到无法打开TikTok网页版的问题,如页面加载失败、显示网络错误或提示访问受限。本文将帮助你快速排查问题,并提供解决方案,让你顺利访问…...
本地部署【LLM-deepseek】大模型 ollama+deepseek/conda(python)+openwebui/docker+openwebui
通过ollama本地部署deepseek 总共两步 1.模型部署 2.[web页面] 参考官网 ollama:模型部署 https://ollama.com/ open-webui:web页面 https://github.com/open-webui/open-webui 设备参考 Mac M 芯片 windows未知 蒸馏模型版本:deepseek-r1:14b 运行情况macminim2 24256 本地…...
【vs2022配置cursor】
Cursor搭配cmake实现C程序的编译、运行和调试的参考地址 cursor下载地址 第一步: 电脑上按爪cmake 第二步:cursor 配置 安装中文 第三步环境变量: D:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Tools\MSVC\14.35.322…...
Redis 的缓存雪崩、缓存穿透和缓存击穿详解,并提供多种解决方案
本文是对 Redis 知识的补充,在了解了如何搭建多种类型的 Redis 集群,并清楚了 Redis 集群搭建的过程的原理和注意事项之后,就要开始了解在使用 Redis 时可能出现的突发问题和对应的解决方案。 引言:虽然 Redis 是单线程的…...
Docker使用指南与Dockerfile文件详解:从入门到实战
Docker使用指南与Dockerfile文件详解:从入门到实战 文章目录 **Docker使用指南与Dockerfile文件详解:从入门到实战****引言****第一部分:Docker 核心概念速览****1. Docker 基础架构****2. Docker 核心命令****第二部分:Dockerfile 文件深度解析****1. Dockerfile 是什么?…...
如何在个人电脑本地化部署Deepseek-R1大模型
文章目录 Deepseek概述公司简介DeepSeek模型优势DeepSeek模型发展历史Ollama安装Deepseek版本选择Deepseek支持的客户端工具编程语言客户端库桌面客户端插件类其他工具客户端工具配置cherryStudio配置测试如何使用DeepSeek满血版什么是 DeepSeek R1 满血版?deepseek官方第三方…...
DeepSeek-R1复现方案梳理
open-r1 项目地址:https://github.com/huggingface/open-r1 由huggingface组建,目前刚上线2周,发布了最新进展open-r1/update-1,在MATH-500任务上接近deepseek的指标,可以在open-r1/open-r1-eval-leaderboard查看指标的…...
【Redis】 - Redis的Bitmap实现用户签到
Redis的Bitmap实现用户签到 使用Redis的Bitmap数据结构来记录用户的每日签到状态是一种高效且节省空间的方法。通过将用户ID和日期结合生成动态Key,可以轻松管理不同用户在不同日期的签到情况。下面详细介绍如何设计这一方案。 设计思路 动态Key生成:根…...
用php tp6对接钉钉审批流的 table 表格 明细控件 旧版sdk
核心代码 foreach ($flows[product_list] as $k>$gift) {$items_list[] [[name > 商品名称, value > $gift[product_name] ?? ],[name > 规格, value > $gift[product_name] ?? ],[name > 数量, value > $gift[quantity] ?? ],[name > 单位, v…...
