docker storage

docker 存储

docker 有3种方式使用系统存储:

  • bind:
    宿主机目录映射方式,启动容器命令添加参数-v dir1:dir2。将宿主机的目录dir1挂载到容器的dir2目录,实际执行的mount -o bind类似命令
  • volume:
    由docker管理的存储,先创建后使用,且可以在多个容器中共享,是官方推荐的方式
  • tmpfs:
    使用宿主机的内存作为存储,这种使用内存文件系统来挂载到容器目录, 使用参数--mount type=tmpfs,destination=/dir, tmpfs是退出即释放的一种文件系统

docker使用的文件系统

docker使用的文件系统经过很多变化,而且在各发行版下可能不同,但目前主流的是overlay2,执行docker info 查看当前使用的是overlay2

1
2
sudo docker info | grep  Storage                                                                                                                                              
Storage Driver: overlay2

除了overlay2,还有aufs(ubuntu),devicemapper(centos),btrfs和zfs。他们的实现都不同,都能支持分层和支持写时复制(Cow/copy-on-write),而他们实现的方式有区别,所以效率也有区别

  • 分层:
    镜像都是分层的,在Dockerfile构建时,每次执行COPY/RUN时,都会增加一层
  • 写时复制:
    在容器或者DockerFile执行修改操作时,包括权限修改,会将lower layer 的文件复制到container层再修改

而容器就是在镜像顶层压栈了一个可写层,而且是临时的,当容器销毁时,这层的文件也会删除

overlay的优势

  1. page caching, 可以在多个不同实例之间共享
  2. 不同层之间,相同文件使用硬连接, 节省inode 和 大小

写时复制 copy-up 会导致第一次写时造成延迟,特别是大文件,拷贝起来费时。 但第二次就不会延时, 而且overlay2 有caching, 相比其它文件系统,更减少延时

overlay的问题

  1. 实现不够完全, 例如没有实现uname
  2. 先只读打开一个文件 open(read), 再读写打开相同文件open(write), 两个fd 会对应2个不同文件, 第一个对应的lower的文件,第二个造成写时复制,对应容器里的文件。
    • 规避方法是先执行touch 操作。 现实的例子是 yum 需要安装yum-plugin-ovl。 但这个只有7.2才支持, 之前的话就需要先touch /var/lib/rpm/*

最佳实践

  1. 使用ssd
  2. 对于写操作比较多的场景,例如数据库,应使用映射文件(bind)或者volume。这样跳过了overlay的复杂操作,直接使用主机的文件系统

overlay的增删改

当运行docker容器时查看挂载

1
overlay on /var/lib/docker/overlay2/04ea1faa8074e5862f40eecdba968bd9b7f222cb30e5bf6a0b9a9c48be0940f2/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B74PWZCBMRCWXFH5UL2ZXB5WEU:/var/lib/docker/overlay2/l/WNHICVPVSDNUGSCZW435TPSMOK,upperdir=/var/lib/docker/overlay2/04ea1faa8074e5862f40eecdba968bd9b7f222cb30e5bf6a0b9a9c48be0940f2/diff,workdir=/var/lib/docker/overlay2/04ea1faa8074e5862f40eecdba968bd9b7f222cb30e5bf6a0b9a9c48be0940f2/work)

docker 将镜像的文件挂载为只读, 将容器层挂载为可读可写。 文件系统可以分为2部分
upper(容器层) + lower (镜像层)

  • 当在容器里执行写时, 如果文件不存在, 会依次遍历lower。如果都不存在就会在upper层创建文件
  • 读也相同
  • 删除时会创建一个without 来隐藏, 这是为什么即使删除容器里的文件, 镜像还是会增大。
  • 删除目录情况也差不多

特殊情况

在保存容器后(docker commit),会多一层,里面包含了修改的文件,以及删除后生成的without文件,然后生成镜像

但对于以下特殊目录文件不会提交, 因为这些文件是运行时docker 要根据用户配置进行修改的。

  1. /etc/hostname
  2. /etc/hosts
  3. /etc/resov.conf

例如docker 的link选项,会在容器的hosts 文件里定义对应的容器名->容器ip

手动mount overlayfs的例子

  1. 原本目录,文件都分散在不同目录ABC
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .
    ├── A
    │ ├── aa
    │ └── a.txt
    ├── B
    │ ├── a.txt
    │ └── b.txt
    ├── C
    │ └── c.txt
    └── worker
    └── work [error opening dir]
  2. overlay 挂载到/tmp/test目录 sudo mount -t overlay overlay -o lowerdir=A:B,upperdir=C,workdir=worker /tmp/test/
  3. 查看test目录
    1
    2
    3
    4
    5
    /tmp/test/
    ├── aa
    ├── a.txt
    ├── b.txt
    └── c.txt
    1
    2
    mount  | grep 'overlay'
    overlay on /tmp/test type overlay (rw,relatime,lowerdir=A:B,upperdir=C,workdir=worker)

参考

https://docs.docker.com/storage/storagedriver/

c++ 存储周期、链接和作用域

c++中变量和函数的三个重要属性

存储周期类型: 有关变量的创建和销毁
链接类型: 有关变量函数的内存位置
作用域: 有关变量函数的可见范围

本文讨论的标识符,包括变量和函数

存储说明符

存储说明符控制变量何时分配和释放,有以下几种

  • automatic
  • thread_local
  • static
  • register
  • mutable
  • extern

说明

  • automatic: 最常见的局部变量,且没有声明为static或者thread_local,位于栈上, 随着代码块的执行和结束而自动分配和销毁
  • static: 静态变量, 在程序启动和结束时创建和销毁,但初始化是在第一次执行初始化代码时执行
  • thread: 在线程开始和结束时分配和销毁
  • dynamic: 最常见的堆上的变量, 需要执行new和delete,

auto 在c++11中不是声明存储周期,而是类型推导符, 但这种存储周期类型的依然存在(局部变量)

初始化的时机

  • automatic: 必须手动初始化,换句话说局部变量必须初始化,否则值为不确定
  • static: 在执行时初始化,且初始化一次,特殊情况下在执行前初始化
  • thread: 因为thread_local变量自带static性质,所以认为其同于static
  • dynamic: 在new时初始化

Linkage

标识符(变量&函数)用一块内存里的值或者函数体来表示的, 而linkage决定其他相同的标识符是否指向同一块内存。c/c++有3种linkage, no-linkage, internal linkage和external linkage

  • no linkage 局部变量没有linkage, 所以两个a是独立的,后面的a会覆盖前面的a,不相干。此时linkage与可见域(scope)类似
  • internal linkage 表示只能在文件内部访问(file scope),换句话就是不会暴露给链接器, 用修饰符static声明internal linkage,所以允许在不同文件声明两个名称&类型相同的internal linkage 标识符,他们指向不同的内存单元。
  • external linkage 表示可以在程序所有地方访问,包括外部文件(global scope),所以是真“全局”(scope&linkage), 所有标识符指向独一份内存。

修饰符

  • 全局const变量和全局constexpr变量默认具备internal linkage, 再加上static没有影响
  • 全局非const变量默认是external linkage, 故再加上extern没有影响。在其他文件使用extern声明这个变量,就能使用指向同一内存的变量
  • 函数默认external linkage,故再加上extern没有影响。 在其他文件使用extern声明这个函数(可省),就能使用指向同一内存的函数
  • 使用extern修饰全局const变量和constexpr变量可以使起具备external linkage

可见staticextern即表示存储周期,又表示linkage, static相对简单,extern则比较复杂,如以下情况

1
2
3
4
5
6
int g_x = 1; // 定义有初始化的全局变量(可加可不加extern)
int g_x; // 定义没有初始化的全局变量(不可加extern),可选初始化
extern int g_x; // 前置声明一个全局变量,不可初始化

extern const int g_y { 1 }; // 定义全局常量,const必须初始化
extern const int g_y; // 前置声明全局常量,不可初始化

所以若是定义未初始化的全局变量,不能加extern,不然就成了前置声明了。

constexpr 特殊情况

虽然通过给constexpr添加extern修饰符来让其具备external属性,但不能在其他文件前置声明。因为constexpr是在编译期替换的,编译器(compile)的可见域限定在文件内,所以编译期无法知道constexpr的值,所以在编译期无法获取到其内存单元的值, 也就无法在其他文件进行声明,只能定义。

file scope和global scope

局部变量的scope、no-linkage以及duration相同,从{开始到}结束。 理论上global scope涵盖了file scope。而linkage来规定其是否能在其他文件里使用。

local class

local class 不允许有static data member

参考

https://en.cppreference.com/w/cpp/language/storage_duration

buffer-cache too high

Linux 调优

系统原厂商是不喜欢讨论系统调优的,一方面说起来没完没了,二来比较复杂,而且私以为调优即说明系统默认不够好?

而且SUSE的原厂规定:

原理机制的介绍及系统调优并不在我们的技术支持范畴

这里是一点相关介绍

buffer/cache 的作用和区别

buffer是用于存放将要输出到disk(块设备)的数据,而cache是存放从disk上读出的数据。二者都是为提高IO性能而设计的。

  • buffer:缓冲将数据缓冲下来,解决速度慢和快的交接问题;速度快的需要通过缓冲区将数据一点一点传给速度慢的区域。
    例如:从内存中将数据往硬盘中写入,并不是直接写入,而是缓冲到一定大小之后刷入硬盘中。
    A buffer is something that has yet to be “written” to disk.

  • cache:缓存实现数据的重复使用,速度慢的设备需要通过缓存将经常要用到的数据缓存起来,缓存下来的数据可以提供高速的传输速度给速度快的设备。
    例如:将硬盘中的数据读取出来放在内存的缓存区中,这样以后再次访问同一个资源,速度会快很多。
    A cache is something that has been “read” from the disk and stored for later use.

总之buff和cache都是内存和硬盘之间的过渡,前者是写入磁盘方向,而后者是写入内存方向

回收cache

1
2
3
drop_caches回收一下。
#sync;sync;sync
#echo 3 > /proc/sys/vm/drop_caches

free增加300M

swap 介绍

Swap意思是交换分区,是硬盘中的一个分区。内核将内存Page移出内存到swap分区(swap out)

swap通过 vm.swappiness 这个内核参数控制,默认值是60。cat /proc/sys/vm/swappiness 可以查看当前值
这个参数控制内核使用swap的优先级。该参数从0到100。

设置该参数为0,表示只要有可能就尽力避免交换进程移出物理内存;
设置该参数为100,这告诉内核疯狂的将swapout物理内存移到swap分区。
注意:设置该参数为0,并不代表禁用swap分区,只是告诉内核,能少用到swap分区就尽量少用到,设置vm.swappiness=100的话,则表示尽量使用swap分区。

这里面涉及到当然还涉swappiness及到复杂的算法。如果以为所有物理内在用完之后,再使用swap, 实事并不是这样。以前曾经遇到过,物理内存只剩下10M了,但是依然没有使用Swap交换空间,另外一台服务器,物理内存还剩下15G,居然用了一点点Swap交换空间。
其实少量使用Swap交换空间是不会影响性能,只有当内存资源出现瓶颈或者内存泄露,进程异常时导致频繁、大量使用交换分区才会导致严重性能问题。

问题:何时使用swap

这个问题如上面说的,比较难说,理论上是当物理内存不够用的时候,又需要读入内存时,会将一些长时间不用的程序的内存Page 交换出去。
但是很多时候会发现,内核即使在内存充足的情况下也是使用到swap

问题: 那些东西被swap了?

可以看下面的测试

回收swap

swapoff 之后执行sudo sysctl vm.swappiness=0 临时让内核不用swapout

1
2
3
并把swap的数据加载内存,并重启swap 
#swapoff -a
#swapon -a

即把swap分区清空, 自测效果如下,内核版本5.10.0-8-amd64

1
2
3
               total        used        free      shared  buff/cache   available
Mem: 12162380 4911564 5605744 459364 1645072 6466572
Swap: 1000444 763040 237404

重启swap后

1
2
3
               total        used        free      shared  buff/cache   available
Mem: 12162380 5605800 4843176 524984 1713404 5707112
Swap: 1000444 0 1000444

可见,停用swap后,swap的used大部分到了mem的used,小部分到了Mem的shared

调优的一些有效工具

perf + flame火焰图: 查看运行耗时,可以查看函数调用耗时,如果是自己的程序,可以知道哪些函数需要优化
vmstat 查看磁盘io情况,使用vmstat -t 3命令,如果b状态的数字一直很大,那么说明磁盘阻塞严重,可能是磁盘坏了,可能是程序设计不合理

还有top,iperf等等

grpc 学习

rpc

rpc 意为远程过程调用, http, grpc 广义上讲都是rpc。
而且还有个项目叫grpc-gateway, 可以将grpc通过http的方式暴露。

grpc

grpc 是rpc的一种实现,由google开源,其他还有thrift, sogorpc 等等。 并且grpc使用的http/2协议

http/1.1 与 http/2 的区别

  • 2使用二进制,而1.1使用文本,提高效率
  • 2将相同的tcp连接合并为一个请求,提高性能,而1.1则为每个请求创建tcp连接
  • 2的客户端使用流,这样可以多次请求
  • 2含有trailers,也就是尾部消息,可以用来发送body的checksume等, 当然也可以直接放到body里

而1.1中也已经实现服务端到客户端的流,使用’Transfer-Encoding=chunked’来替代’Content-Length’,详见rfc

1
2
A sender MUST NOT send a Content-Length header field in any message
that contains a Transfer-Encoding header field.

认识proto文件

proto 文件中多个service和单个service 区别

在同一个service里的方法会codegen到同一个类,但这个类比较鸡肋。
由于RPC调用是RESTful的,所以多次调用或者多个rpc方法无法通过同一个service来共享数据,这需要使用者借助其他办法来解决。

service 还可以用以隔离相同名称的rpc, 如

  • service1/helloworld
  • service2/helloworld

而方法和方法通过RpcServiceMethod来保存,而通过index来调用

1
2
::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
::grpc::Service::RequestAsyncUnary(1, context, request, response, new_call_cq, notification_cq, tag);

rpc 声明UnaryCall&StreamingCall

非流调用也称为UnaryCall,指发送或接受的消息大小是固定的。
流调用称为StreamingCall,可以多次发送或者接收,所以消息大小并不固定。

StreamCall 可以多次调用,直到发送WriteDone/Finish,所以在接受的一端总是

1
while(read stream){}

grpc支持客户端流服务端非流、客户端非流、服务端流以及双向流,而普通的就是客户端和服务端都不流NORMAL_RPC(unary call)

  • grpc::internal::RpcMethod::NORMAL_RPC
  • grpc::internal::RpcMethod::RpcType::SERVER_STREAMING
  • grpc::internal::RpcMethod::RpcType::CLIENT_STREAMING
  • grpc::internal::RpcMethod::RpcType::BIDI_STREAMING

认识pb.h和grpc.pb.h文件

protoc 调用grpc_cpp_plugin 插件生成grpc.pb.{h,cc}文件,生成rpc方法的实现

pb.{h,cc}则是定义了protobuf消息的序列化和反序列化方法

反射、序列化和反序列化的实现

pb.h 实现grpc的请求参数和返回参数的特定语言的解析,还有pb的通用方法,
例如: has_xx(版本3里只有自定义类型才支持), class XXX_CPP_API

生成的class都继承自google::protobuf::Message

1
2
3
4
5
class HelloRequest PROTOBUF_FINAL :
public ::PROTOBUF_NAMESPACE_ID::Message

#define PROTOBUF_NAMESPACE "google::protobuf"
#define PROTOBUF_NAMESPACE_ID google::protobuf

而在message中有注释说明, 关键函数是SerializeToStringParseFromString,还有个array版本SerializeToArray,
还有一个反射函数GetDescriptor()用来动态获取指定槽位的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Example usage:
//
// Say you have a message defined as:
//
// message Foo {
// optional string text = 1;
// repeated int32 numbers = 2;
// }
//
// Then, if you used the protocol compiler to generate a class from the above
// definition, you could use it like so:
//
// std::string data; // Will store a serialized version of the message.
//
// {
// // Create a message and serialize it.
// Foo foo;
// foo.set_text("Hello World!");
// foo.add_numbers(1);
// foo.add_numbers(5);
// foo.add_numbers(42);
//
// foo.SerializeToString(&data);
// }
//
// {
// // Parse the serialized message and check that it contains the
// // correct data.
// Foo foo;
// foo.ParseFromString(data);
//
// assert(foo.text() == "Hello World!");
// assert(foo.numbers_size() == 3);
// assert(foo.numbers(0) == 1);
// assert(foo.numbers(1) == 5);
// assert(foo.numbers(2) == 42);
// }

如下可以将Message转换为基本类型

1
2
3
int size = reqMsg.ByteSizeLong();
char* array = new char[size];
reqMsg.SerializeToArray(array, size);
1
2
3
std::string bytes = reqMsg.SerializeAsString();
const char* array = bytes.data();
int size = bytes.size();

进一步看protobuf::message继承自protobuf::message_lite, 后者实现了SerializeAsStringSerializeToArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
inline uint8* SerializeToArrayImpl(const MessageLite& msg, uint8* target,
int size) {
constexpr bool debug = false;
if (debug) {
// Force serialization to a stream with a block size of 1, which forces
// all writes to the stream to cross buffers triggering all fallback paths
// in the unittests when serializing to string / array.
io::ArrayOutputStream stream(target, size, 1);
uint8* ptr;
io::EpsCopyOutputStream out(
&stream, io::CodedOutputStream::IsDefaultSerializationDeterministic(),
&ptr);
ptr = msg._InternalSerialize(ptr, &out);
out.Trim(ptr);
GOOGLE_DCHECK(!out.HadError() && stream.ByteCount() == size);
return target + size;
} else {
io::EpsCopyOutputStream out(
target, size,
io::CodedOutputStream::IsDefaultSerializationDeterministic());
实际调用-> auto res = msg._InternalSerialize(target, &out);
GOOGLE_DCHECK(target + size == res);
return res;
}
}

可见,其实序列化最终调用的是pb.h文件里定义的_InternalSerialize, 举例官方例子HelloRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
::PROTOBUF_NAMESPACE_ID::uint8* HelloRequest::_InternalSerialize(
::PROTOBUF_NAMESPACE_ID::uint8* target, ::PROTOBUF_NAMESPACE_ID::io::EpsCopyOutputStream* stream) const {
// @@protoc_insertion_point(serialize_to_array_start:helloworld.HelloRequest)
::PROTOBUF_NAMESPACE_ID::uint32 cached_has_bits = 0;
(void) cached_has_bits;

// string name = 1;
if (this->name().size() > 0) {
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::VerifyUtf8String(
this->_internal_name().data(), static_cast<int>(this->_internal_name().length()),
::PROTOBUF_NAMESPACE_ID::internal::WireFormatLite::SERIALIZE,
"helloworld.HelloRequest.name");
target = stream->WriteStringMaybeAliased(
1, this->_internal_name(), target);
}

if (PROTOBUF_PREDICT_FALSE(_internal_metadata_.have_unknown_fields())) {
target = ::PROTOBUF_NAMESPACE_ID::internal::WireFormat::InternalSerializeUnknownFieldsToA rray(
_internal_metadata_.unknown_fields<::PROTOBUF_NAMESPACE_ID::UnknownFieldSet>(::PROTOB UF_NAMESPACE_ID::UnknownFieldSet::default_instance), target, stream);
}
// @@protoc_insertion_point(serialize_to_array_end:helloworld.HelloRequest)
return target;
}

grpc.pb生成的代码实现rpc调用

生成的框架代码用来继承实现Service和获取stub来发起rpc call。实际上这些代码并不是必须的
在下面讲了如何使用几个工厂类来创建Stub,还有直接new出Service

1
2
3
4
5
6
7
8
9
10
11
12
13
class XXXServer {
// 客户端使用的桩
class Stub
// base
class Service
// 各种版本的rpc包装,但都继承自base
class WithAsyncMethod_XXX
typedef WithAsyncMethod_XXX<Service > AsyncService;
typedef ExperimentalWithCallbackMethod_XXX<Service > CallbackService;
class WithGenericMethod_XXX
class WithRawMethod_XXX
typedef WithStreamedUnaryMethod_XXX<Service > StreamedUnaryService;
}

同步与异步

grpc 的异步即为使用cq事件驱动(cq-based),使用tag标记事件。另外还有callback方式

对于客户端

同步时,通过调用’::grpc::internal::BlockingUnaryCall’
异步时,创建’ClientAsyncResponseReader’(非流), 然后通过调用’ClientAsyncResponseReader’的write和finish,并等待tag
当存在流时分别是

  • ::grpc::ClientAsyncReader
  • ::grpc::ClientAsyncWriter
  • ::grpc::ClientAsyncReaderWriter

这些类型可用对应的工厂类来创建, 生成代码的stub也是这么用的

1
2
3
class ClientReaderFactory 
class ClientWriterFactory
class ClientReaderWriterFactory

对于服务端

同步时,通过’AddMethod’来注册,生成代码会在父类构造时执行。注册后由grpc调用

1
2
3
4
5
6
7
8
9
10
11
12
Greeter::Service::Service() {
AddMethod(new ::grpc::internal::RpcServiceMethod(
Greeter_method_names[0],
::grpc::internal::RpcMethod::NORMAL_RPC,
new ::grpc::internal::RpcMethodHandler< Greeter::Service, ::helloworld::HelloRequest, ::helloworld::HelloReply>(
[](Greeter::Service* service,
::grpc_impl::ServerContext* ctx,
const ::helloworld::HelloRequest* req,
::helloworld::HelloReply* resp) {
return service->SayHello(ctx, req, resp);
}, this)));
}

异步时,类似客户端

  • grpc::ServerAsyncReaderWriter
  • grpc::ServerAsyncReader
  • grpc::ServerAsyncWriter

可见服务端是直接new出来的,异步时这些io操作对象也是直接new出来的, 在调用以下时传入

1
2
3
RequestAsyncBidiStreaming
RequestAsyncClientStreaming
RequestAsyncServerStreaming

grpc callback

只在客户端使用,callback方式的请求可以传入一个lambda, 在请求完成时调用

1
2
3
4
5
6
7
stub_->async()->SayHello(&context, &request, &reply,
[&mu, &cv, &done, &status](Status s) {
status = std::move(s);
std::lock_guard<std::mutex> lock(mu);
done = true;
cv.notify_one();
});

新版本的grpc已经将实验性的标记去除,说明此方式成熟了

1
2
3
4
5
#ifdef GRPC_CALLBACK_API_NONEXPERIMENTAL
::grpc::Service::
#else
::grpc::Service::experimental().
#endif

grpc异步流

官方仓库的示例代码没有异步且流的, 在实际项目中用到异步流,使用大概方法

  1. 手动创建writereader
  2. 启动时,调用’grpc::Service::RequestAsyncBidiStreaming’ 和 ‘grpc::Service::RequestAsyncClientStreaming’ 以及’RequestAsyncServerStreaming’, 向cq塞请求new_connection事件
  3. 收到’new_connection’事件返回后,再调用read事件。

一共有5个类型

1
new_connection, read, write, finish, done

我写了一个demo grpcstreamhelloworld

grpc 消息大小

老版本的grpc中,发送端是支持无限大小的,但接受端只能是4M

1
2
#define GRPC_DEFAULT_MAX_SEND_MESSAGE_LENGTH -1
#define GRPC_DEFAULT_MAX_RECV_MESSAGE_LENGTH (4 * 1024 * 1024)

服务端代码

1
2
3
4
std::unique_ptr<Server> ServerBuilder::BuildAndStart() {
if (max_receive_message_size_ >= 0) {
args.SetInt(GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH, max_receive_message_size_);
}

但在新版grpc中变了

1
2
3
4
5
6
7
8
std::unique_ptr<grpc::Server> ServerBuilder::BuildAndStart() {
grpc::ChannelArguments args;
if (max_receive_message_size_ >= -1) {
args.SetInt(GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH, max_receive_message_size_);
}
if (max_send_message_size_ >= -1) {
args.SetInt(GRPC_ARG_MAX_SEND_MESSAGE_LENGTH, max_send_message_size_);
}

grpc 编译安装的问题

https://github.com/grpc/grpc/issues/13841

grpc异步存在问题

因为异步服务端通过completionqueue来通知rpc执行结果和执行下次调用,通常使用多queue和多线程的方式提高处理效率

  1. 通常情况是多queue, 即每个service对应一个queue, 而每个service又有多个rpc,线程去轮询这个complete_queue。这样导致高线程切换开销,而且complete_queue也占用大量内存
  2. 多线程,queue可以用多个线程去轮询,但0.13版本可能出现bug

grpc异步流存在的问题

grpc区别与其他框架很大一个优势是支持异步流,即可以多次请求和多次回复。异步是基于cq的事件驱动,所以必须等待tag回调, 连续两次发送会异常。
而真正的请求一般在业务模块处理, 不知道tag的状态即不知道是否正在发送, 那么如何在cq回调外发送消息呢?

办法是维护一个发送队列,消息先存队列里,等待cq回调时取出发送。 另外由于流同步需要显式发送结束标记(服务端调Stream::Finish, 客户端调用WriteDown和Finish),
所以需要有一个特殊消息加以区分,通常用空指针,也可以设置结束标志。另外由于发送代码会同时被业务调用和cq回调,需要对发送代码加锁

调试grpc

通过设置环境变量,让grpc向控制台打印详细信息

1
2
3
4
5
export GRPC_VERBOSITY=DEBUG
bash-5.0# ./build/bin/hasync slave stdin stdout @127.0.0.1:7615
D1026 08:27:44.142802149 24658 ev_posix.cc:174] Using polling engine: epollex
D1026 08:27:44.143406685 24658 dns_resolver_ares.cc:490] Using ares dns resolver
I1026 08:27:44.158115785 24658 server_builder.cc:332] Synchronous server. Num CQs: 1, Min pollers: 1, Max Pollers: 2, CQ timeout (msec): 10000

项目实践

项目使用客户端异步/同步,服务端全异步, 可以兼容四种传输方式

引用

https://grpc.github.io/grpc/cpp/grpcpp_2impl_2codegen_2sync__stream_8h_source.html
https://grpc.github.io/grpc/cpp/grpcpp_2impl_2codegen_2byte__buffer_8h_source.html
https://grpc.github.io/grpc/cpp/call__op__set_8h_source.html

cmake 使用第三方库

cmake 使用第三方库

在项目中链接第三方库的方法都是’target_include_directories’ 和 ‘target_link_library’, 前提引入第三方包. 而查找可以使用find_package

find_package找包

find_package分为Module和Config两种方式

Module方式

find_package先在’/usr/share/cmake/Modules/Find/‘下添加FindXXX.cmake文件,以及自定义路径(CMAKE_MODULE_PATH)下查找
然后在项目的CMakeList.txt中使用find_package(), 然后可以在链接的时候使用第三方库

1
find_package()

Config模式

当find_package找不到FindXXX.cmake文件,则会找

  • Config.cmake
  • -config.cmake

如果第三方项目支持cmake, 那么先通过cmake编译和安装到环境或者docker环境,这时会在’/usr/lib/cmake//‘下添加上述文件

安装FindXXX.cmake文件

当没有FindXXX.cmake时,可以使用安装包管理工具安装cmake-extra包, 可能找到需要的

1
$ pacman -S extra-cmake-modules

然后执行下面的命令,可以看到大量的’Find*.cmake’文件

1
ls /usr/share/cmake-3.20/Modules/

自定义FindXXX.cmake文件

如果上述方式都不行,那么需要自己写FindXXX.cmake,放到CMAKE_MODULE_PATH下
例如在项目根目录创建文件夹cmake_module, 再使用set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake_module)来指定module的路径
最后在’cmake_module’下创建FindXXX.cmake结尾的文件,这个文件用来写找header和lib规则, 内容大致为

1
2
3
4
5
find_path(Grpc_INCLUDE_DIR grpc++/grpc++.h)
mark_as_advanced(Grpc_INCLUDE_DIR)

find_library(Grpc++_LIBRARY NAMES grpc++ grpc++-1-dll)
mark_as_advanced(Grpc++_LIBRARY)

有这个文件之后,可以在项目的cmake中直接使用find_package()

源代码编译链接

将第三方库源码放到项目指定目录如third

  1. 放到third目录并可以使用git submodule管理
  2. 在thrid目录添加CMakeList.txt,在其中添加目标,已备在项目中链接
    1
    2
    3
    # for gsl-lite target
    add_library(gsl-lite INTERFACE)
    target_include_directories(gsl-lite SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/gsl-lite/include)

FetchContent 自动源代码链接

cmake3.11之后,可以使用这个办法来自动拉取网上的库,并可以直接在自己的项目中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# NOTE: This example uses cmake version 3.14 (FetchContent_MakeAvailable).
# Since it streamlines the FetchContent process
cmake_minimum_required(VERSION 3.14)

include(FetchContent)

# In this example we are picking a specific tag.
# You can also pick a specific commit, if you need to.
FetchContent_Declare(GSL
GIT_REPOSITORY "https://github.com/microsoft/GSL"
GIT_TAG "v3.1.0"
)

FetchContent_MakeAvailable(GSL)

# Now you can link against the GSL interface library
add_executable(foobar)

# Link against the interface library (IE header only library)
target_link_libraries(foobar PRIVATE GSL)

##cmake使用openssl存在问题

因为openssl不用cmake,也就没有.cmake文件, 导致项目配置失败

1
2
3
Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the
system variable OPENSSL_ROOT_DIR (missing: OPENSSL_LIBRARIES
OPENSSL_INCLUDE_DIR)

后面发现它是使用package_config方式

1
2
3
4
5
6
7
8
9
10
#/usr/local/lib/pkgconfig/openssl.pc
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: OpenSSL
Description: Secure Sockets Layer and cryptography libraries and tools
Version: 1.1.1k
Requires: libssl libcrypto

这种情况除了通过cmake_module来解决之外,还可以通过指定pc文件的路径

1
cmake -DOPENSSL_ROOT_DIR=/usr/local/

ExternalProject_Add

这个不常用

find_package 和 find_library 区别

find_library 是cmake的底层方法,在find_path指定的目录下查找库文件
而find_package 使用了find_library的来找库文件,而且find_package在找到目标后,会定义一些变量,如下面的’Findlibproxy.cmake’文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# - Try to find libproxy
# Once done this will define
#
# LIBPROXY_FOUND - system has libproxy
# LIBPROXY_INCLUDE_DIR - the libproxy include directory
# LIBPROXY_LIBRARIES - libproxy library
#
# Copyright (c) 2010, Dominique Leuenberger
#
# Redistribution and use is allowed according the license terms
# of libproxy, which this file is integrated part of.

# Find proxy.h and the corresponding library (libproxy.so)
FIND_PATH(LIBPROXY_INCLUDE_DIR proxy.h )
FIND_LIBRARY(LIBPROXY_LIBRARIES NAMES proxy )

当找到libproxy.so的时候,LIBPROXY_FOUND被设置为TRUE等

urxvt配置

前言

自使用arch以来,一直在用urxvt, 它简洁,轻量,但不可否认的有问题,比如中文输入模式长时间时会无法输入中文,配置麻烦, 需要在启动脚本配置.xresource
这里要记一下自己的urxvt的配置以做备份

使用urxvt的主要功能

urxvt 非常简洁的tab功能,支持多路复用以及右键菜单格式化字符串,而且支持假透明,非常轻量。

urxvt不够现代化,不是开箱即用的,需要如下修改:

  • tab功能需要修改perl的包,因为默认情况下不支持切换tab
  • 不支持icon, 需要在配置文件手动指定icon的位置
  • tab功能需要额外启动参数,所以顺便编一个desktop启动文件

perl修改

复制/usr/lib/perl/ext/tabbed到用户目录~/.urxvt/ext/, 修改tab_key_press函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# if ($keysym == 0xff51 || $keysym == 0xff53)  表示使用ctrl+shift 和方向键来移动tab
sub tab_key_press {
my ($self, $tab, $event, $keysym, $str) = @_;

if ($event->{state} & urxvt::ShiftMask && !($event->{state} & urxvt::ControlMask) ) {
if ($keysym == 0xff51 || $keysym == 0xff53) {
my ($idx) = grep $self->{tabs}[$_] == $tab, 0 .. $#{ $self->{tabs} };

--$idx if $keysym == 0xff51;
++$idx if $keysym == 0xff53;

$self->make_current ($self->{tabs}[$idx % @{ $self->{tabs}}]);

return 1;
} elsif ($keysym == 0xff54) {
$self->new_tab;

return 1;
}
}elsif ($event->{state} & urxvt::ControlMask && $event->{state} & urxvt::ShiftMask) {
if ($keysym == 0xff51 || $keysym == 0xff53) {
my ($idx1) = grep $self->{tabs}[$_] == $tab, 0 .. $#{ $self->{tabs} };
my $idx2 = ($idx1 + ($keysym == 0xff51 ? -1 : +1)) % @{ $self->{tabs} };

($self->{tabs}[$idx1], $self->{tabs}[$idx2]) =
($self->{tabs}[$idx2], $self->{tabs}[$idx1]);

$self->make_current ($self->{tabs}[$idx2]);

return 1;
}
}

()
}

urxvt 启动文件

创建启动文件,使其默认为tab模式 ‘.local/share/applications/urxvtq.desktop’

1
2
3
4
5
6
7
8
9
[Desktop Entry]
Version=1.0
Name=urxvtq
Comment=An unicode capable rxvt clone
Exec=urxvt -pe tabbed
Icon=utilities-terminal
Terminal=false
Type=Application
Categories=System;TerminalEmulator;

urxvt 配置

创建如下的文件,并要在合适的启动脚本里添加一行[ -f "$HOME/.Xresources" ] && xrdb -merge "$HOME/.Xresources"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
!!$HOME/.Xresources

!! dbi
Xft.dpi:98

/* Couleurs Tango */

!! 下划线色
URxvt.colorUL: #87afd7
URxvt.colorBD: white
URxvt.colorIT: green

!! tab 配色
URxvt.tabbed.tabbar-fg: 2
URxvt.tabbed.tabbar-bg: 0
URxvt.tabbed.tab-fg: 3
URxvt.tabbed.tab-bg: 2
URxvt.tabbed.tabren-bg: 3
URxvt.tabbed.tabdiv-fg: 8
URxvt.tabbed.tabsel-fg: 1
URxvt.tabbed.tabsel-bg: 8

!! fake transparent
URxvt.transparent: true
URxvt.shading: 10
URxvt.fading: 40
!! font
URxvt.font: xft:Monospace,xft:Awesome:pixelsize=14
URxvt.boldfont: xft:Monospace,xft:Awesome:style=Bold:pixelsize=16

!! scroll behavior
URxvt.scrollBar: false
URxvt.scrollTtyOutput: false
URxvt.scrollWithBuffer: true
URxvt.scrollTtyKeypress: true

!! addtional
URxvt.internalBorder: 0
URxvt.cursorBlink: true
URxvt.saveLines: 2000
URxvt.mouseWheelScrollPage: false

! Restore Ctrl+Shift+(c|v)
URxvt.keysym.Shift-Control-V: eval:paste_clipboard
URxvt.keysym.Shift-Control-C: eval:selection_to_clipboard
URxvt.iso14755: false
URxvt.iso14755_52: false

! alt+s 搜索
URxvt.perl-ext: default,matcher,searchable-scrollback
URxvt.keysym.M-s: searchable-scrollback:start

! url match 问题是tab模式下不支持跳转浏览器
URxvt.url-launcher: /usr/bin/firefox
URxvt.matcher.button: 1


URxvt.termName: xterm-256color
URxvt.iconFile: /usr/share/icons/gnome/32x32/apps/gnome-terminal-icon.png
! fast key
URxvt.keysym.Control-Up: \033[1;5A
URxvt.keysym.Control-Down: \033[1;5B
URxvt.keysym.Control-Left: \033[1;5D
URxvt.keysym.Control-Right: \033[1;5C

最后

用了kitty就不会有输入法的问题,字体也很丰富, 推荐现代化模拟终端kitty

static compile warning if namespace resolving function used

前言

当程序里面有使用到解析函数的时候, 静态编译程序会报warning

现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
../../lib/libhacore.a(File.cpp.o): In function `_ZN2ha4core9gid2groupB5cxx11Ej':
/tmp/src/ha/core/File.cpp:213: warning: Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
../../lib/libhacore.a(File.cpp.o): In function `ha::core::group2gid(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, unsigned int&)':
/tmp/src/ha/core/File.cpp:291: warning: Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
../../lib/libhacore.a(File.cpp.o): In function `ha::core::username2uid(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, unsigned int&)':
/tmp/src/ha/core/File.cpp:271: warning: Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
../../lib/libhacore.a(File.cpp.o): In function `_ZN2ha4core12uid2usernameB5cxx11Ej':
/tmp/src/ha/core/File.cpp:194: warning: Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
CMakeFiles/hasync-exec.dir/main.cpp.o: In function `boost::asio::detail::socket_ops::getaddrinfo(char const*, char const*, addrinfo const&, addrinfo**, boost::system::error_code&)':
/usr/local/include/boost/asio/detail/impl/socket_ops.ipp:3348: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/local/lib/gcc/x86_64-pc-linux-gnu/8.2.0/../../../../lib64/libcrypto.a(b_sock.o): In function `BIO_gethostbyname':
b_sock.c:(.text+0x51): warning: Using 'gethostbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/local/lib/gcc/x86_64-pc-linux-gnu/8.2.0/../../../../lib64/libcares.a(libcares_la-ares_getaddrinfo.o): In function `ares_getaddrinfo':
ares_getaddrinfo.c:(.text+0x73f): warning: Using 'getservbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/local/lib/gcc/x86_64-pc-linux-gnu/8.2.0/../../../../lib64/libcares.a(libcares_la-ares_getnameinfo.o): In function `lookup_service.part.0.constprop.2':
ares_getnameinfo.c:(.text+0x32d): warning: Using 'getservbyport_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

结论

即使静态链接glibc, glibc内部在运行时依旧要调用动态库,这些库函数多与域名解析有关, 所以要求运行时的库版本要与编译时相同, 当然基于容器化部署就不用担心了

智能指针

智能指针

智能指针是c++11加入的特性,包括shared_ptr和unique_ptr,weak_ptr,以及make_shared函数,但make_unique是c++14才出来
不过c++11可以通过模版来实现make_unique

普通指针和智能指针的区别

shared_ptr和unique_ptr都是异常安全的(exception-safe),而普通指针不是,举例如下

1
2
3
4
5
void nonsafe_call(T1* t1, T2* t2);
void safe_call(unique_ptr<T1> t1, unique_ptr<T2> t2);

nonsafe_call(new T1, new T2);
safe_call(make_unique<T1>, make_unique<T2>);

当形参为普通指针时

虽然new是安全的,会先申请内存再构造对象t1。 如果t1的构造函数抛异常,申请的内存会自动释放,不会内存泄漏
但当两个new作为函数参数时,情况不同。 因为参数必须在调用函数前决断,所以步骤如下

1
2
3
4
5
1. 为t1申请内存
2. 构造t1
3. 为t2申请内存
4. 构造t2
5. 调用函数
  • 当执行到2失败时,不会泄漏,new会释放t1的内存
  • 当执行到4失败是,会泄漏t1,因为t1已经构造完成,不会释放内存

当形参为智能指针时

步骤为

1
2
3
1. t1 = make_unique<T1>()
2. t2 = make_unique<T2>()
3. 调用函数

如果2失败,t1的对象由unique_ptr管理,当t1释放时,会释放内存,所以无论t1构造失败还是t2构造失败,都能正确释放内存,不会导致泄漏
就因为unique_ptr和shared_ptr是内存安全的

但以普通指针构造智能指针的方式不是异常安全的

foo(std::unique_ptr<T1>(new T1()), std::unique_ptr<T2>(new T2())); 不是异常安全的

此时步骤可能如下

1
2
3
4
5
6
7
1. 为t1申请内存
2. 构造t1
3. 为t2申请内存
4. 构造t2
5. 构造unique_ptr<T1>
6. 构造unique_ptr<T2>
7. 调用函数

当步骤4发生异常时,t1并未被unique_ptr管理,所以不会去释放t1的内存,故发生内存泄漏

智能指针的构造

shared_ptr对应的函数是make_shared
但c++11中没有make_unique, 可以使用模版实现如下

1
2
3
4
template<typename T,typename ...Args>
std::unique_ptr<T> make_unique(Args&& ...args){
return std::unique_ptr<T>(new T(std::forward<Args>(args)... ));
}

那为什么这样就能异常安全呢?因为调用make_uniqe时,可以确保t1的内存被unique_ptr管理了

make_shared创建智能指针和用new创建智能指针的区别

除了上面说所的make_shared/make_unique是异常安全,而unique_ptr(new T()) 不是异常安全外,其构造的智能指针也不同

智能指针之所以会释放内存,是因为智能指针本身也是一个对象,在其生命周期结束后会调用dtor, 并在那里释放内存
所以智能指针的对象内存即包含了T,也包含了智能指针本身,而make_shared() 和 shared_ptr(new T)的区别在于前者是两个部分的地址连续,而后者并不一定连续

unique_ptr的区别

unique_ptr和shared_ptr不同,后者为了多变量同时拥有资源的访问,而unique_ptr表示任何时刻,只有一个变量能访问资源和释放资源,比如打开的文件设备,
而且通常为管理的资源设置析构函数,比如指定析构时关闭文件和设备

如下代码,当fp析构时,自动关闭文件

1
2
3
4
5
6
7
// helper function for the custom deleter demo below
void close_file(std::FILE* fp)
{
std::fclose(fp);
}
using unique_file_t = std::unique_ptr<std::FILE, decltype(&close_file)>;
unique_file_t fp(std::fopen("demo.txt", "r"), &close_file);

unique_ptr要配合move使用,当move之后,原对象不拥有指针资源

总结

为了避免多参数决断时,导致已经决断的参数内存泄漏,应尽可能使用智能指针来管理内存

使用avahi将传统打印机支持无线打印

前言

自从知道mdns之后,发现它非常方便,例如可以找到lan下有哪些ftp服务,ssh服务等。甚至我配置好的打印机服务。

mdns介绍

有别于dns, mdns专门用来解析链路域名(TLD为*.local), 且不需要域名服务器(nameserver)。
原理是同一个lan里的主机通过avahi或zeroconfig类似的工具,向lan广播自己的ip信息和服务

avahi 介绍

avahi是zeroconfig的开源实现,在linux下还有systemd-resolve 同样可以实现mdns解析,但是avahi依旧是linux预装的软件。

avahi 配置

主要修改/etc/avahi/avahi-daemon.conf, 这里使用了br0和wlan0两个网卡,avahi就可以访问两个网络

1
2
use-ipv4=yes
allow-interfaces=br0,wlan0

再设置自启动

nss 配置

要让主机能解析到*.local 域名,需要修改配置文件/etc/nsswitch.conf

1
2
- hosts: files mymachines myhostname resolve [!UNAVAIL=return] dns
+ hosts: files mymachines myhostname mdns_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns

这样,在主机请求解析域名的时候, 会先去请求avahi解析,如果avahi解析失败(说明不是一个local域名)才会去查找域名解析(dns)

使用systemd-resolved

对于使用systemd-resoved来管理dns解析时,可以直接用sudo systemd-resolve --set-mdns=yes --interface=eth0启动eth0端口的mdns广播。

顺便提一下,用systemd-networkd配置dns服务器和用systemd-resolved配置dns的区别,前者为每个以太网口单独配置,而systemd-resoved会读取每个以太网口配置,另外还有一个全局的dns配置。

测试

首先可以ping 本地local域名

1
2
3
4
~ > ping hst.local                                                                                                                                                                                      
PING hst.local(hst (fe80::3e9c:fff:fe8c:69c3%wlan0)) 56 data bytes
64 bytes from hst (fe80::3e9c:fff:fe8c:69c3%wlan0): icmp_seq=1 ttl=64 time=0.113 ms
64 bytes from hst (fe80::3e9c:fff:fe8c:69c3%wlan0): icmp_seq=2 ttl=64 time=0.144 ms

然后执行avahi-discover-standalone 可以查找到很多*.local的地址,特别是在公司网络里

广告自己的服务

配置好了avahi, 就可以向外界发布自己的服务,让其他人知道,也可以选择不发布
例如要广告我的ssh服务, 只需要将预设的配置文件拷贝到avahi的配置目录下

1
sudo cp /usr/share/doc/avahi/ssh.service /etc/avahi/services

这样,别人就能发现我的ssh服务了

配置打印机

linux的打印功能依赖CUPS这个软件,先安装CUPS,设置自启动,然后就可以访问CUPS的网页版管理界面http://localhost:631
在这个网页中添加网络打印机或者本地打印机,然后测试打印是否正常

最后,配置无线功能

上一部添加的打印机可能是有线网络打印机,也可能是串口打印机,都不能让我们的手机直接使用。

但是通过cups + avahi 就可以将你的有线打印机变成无线打印机了
首先在cups里配置,勾选Share This Printer
cups shares printer

然后像刚才添加ssh服务一样, 添加打印服务
/etc/avahi/services/ 创建一个service后缀的文件airprint.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name>Printer</name>
<service>
<type>_ipp._tcp</type>
<subtype>_universal._sub._ipp._tcp</subtype>
<port>631</port>
<txt-record>txtver=1</txt-record>
<txt-record>qtotal=1</txt-record>
<txt-record>rp=printers/Ricoh_MP_4055</txt-record>
<txt-record>ty=Ricoh_MP_4055</txt-record>
<txt-record>adminurl=http://yourip:631/printers/Ricoh_MP_4055</txt-record>
<txt-record>note=Ricoh_MP_4055</txt-record>
<txt-record>priority=0</txt-record>
<txt-record>product=(GPL Ghostscript)</txt-record>
<txt-record>printer-state=3</txt-record>
<txt-record>printer-type=0x801046</txt-record>
<txt-record>Transparent=T</txt-record>
<txt-record>Binary=T</txt-record>
<txt-record>Fax=F</txt-record>
<txt-record>Color=T</txt-record>
<txt-record>Duplex=T</txt-record>
<txt-record>Staple=F</txt-record>
<txt-record>Copies=T</txt-record>
<txt-record>Collate=F</txt-record>
<txt-record>Punch=F</txt-record>
<txt-record>Bind=F</txt-record>
<txt-record>Sort=F</txt-record>
<txt-record>Scan=F</txt-record>
<txt-record>pdl=application/octet-stream,application/pdf,application/postscript,image/jpeg,image/png,image/urf</txt-record>
<txt-record>URF=W8,SRGB24,CP1,RS600</txt-record>
</service>
</service-group>

根据情况修改一下内容,就完成了, 亲测iphone可以使用这个服务打印

注意

上面的情况假设防火墙处于关闭状态,如果启用防火墙的情况下,需要开放avahi、ssh以及ipp服务

1
2
3
sudo firewall-cmd --add-service ssh ipp --permanent
sudo firewall-cmd --add-port=5353/tcp --permanent
sudo firewall-cmd --reload

cpp-template-programing

模板编程

模板编程是其它高级语言没有的技术, 也称为范型编程,多在stl库使用。这种对类型的泛化相当于在编程之上编程

概述

模板可以作用于函数和类,即能泛化类型,也可以泛化大小

1
2
3
4
5
6
7
8
9
template <typename T, int N> void foo() { T t[N]; }
template <typename T, int N> struct Foo { T t[N]; };

int main() {
// array of two int
foo<int, 2>();
// class has a array of two int
Foo<int, 2> F;
}

模板的特化

跟泛化相反的方向,叫特化,编译器会优先使用特化的版本, 而特化有2个方向,类型特化范围特化

类型特化

对于特化类型个数的不同,分为偏特化和全特化,全特化即为所有类型都指定,特化类型越多匹配优先级越高
注意:模板函数不能偏特化

1
2
3
4
template<typename T1, typename T2> struct Foo{};
template<typename T2> struct Foo<int, T2> {};
// 全特化
template<> struct Foo<int,int>{};

例如`Foo<int, int> foo’,编译器会使用第三个版本

范围特化

比如常见的指针和引用,这和int, float, class 都是无关的,属于另个维度,也可以说是范围, 在stl为兼容指针做大量的工作

1
template <typename T, typename N> struct Foo<T *, N> {};

这样’Foo<int *, int> foo’会使用这个版本

还例如指定对大小的特例化

1
2
3
4
5
//模板
template<int n> foo(){}

//值特例化
template<> foo<10> foo(){}

那么如果调用foo<10>();时,优先匹配特例化版本

函数匹配优先级

在函数调用时,普通函数的匹配优先级高于模板函数

1
2
3
4
5
template <typename T> void f(T) { std::cout << "temp\n"; }
void f(int d) { std::cout << "temp1\n"; }
template <> void f(int d) { std::cout << "temp2\n"; }

f(1); // temp1

自定义类型的范围特化

上面讲的是指针类型和引用类型两种,但如果是自定义类型,那就无穷无尽了,所以模板编程也是’图灵完备’的

例如以下,创建了一个自定义的类型来包装基本类型(int,float),这样可以有自定义类型的特化版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T> struct Decor { using type = T; };
template <typename T> struct Strip { using type = T; };
template <typename T> struct Strip<Decor<T>> { using type = T; };
template <typename T> using StripDecor = typename Strip<T>::type;

template <typename T> class Row {};

int main() {
using nodecor = Row<int>;
using decor = Decor<Row<int>>;
// 虽底层同为int, 但nodecor 类型不同于decor类型
static_assert(std::is_same<col>, nocol>::value);
// 通过Strip取出其底层类型
static_assert(std::is_same<StripDecor<col>, nocol>::value);
return 0;
}

模板的声明定义分离

  1. 由于template用来生成函数和类,所以编译器需要同时知道template的类型和其细节,所以模板函数不支持将定义放到源文件中
  2. 而且编译器通常是以cpp为编译单元,当编译模板cpp时不知道调用cpp, 编译调用cpp时,不知道模板cpp。所以模板函数不支持将定义放到源文件中
  3. 对于模板类可以将成员函数的定义放到源文件,但要为每个成员函数都添加’template’限定, 而且要为实例添加特例化
  4. 显式特例化支持只声明不定义,而在源文件中为每种所需的类型都特例化,即与3相同。其实显示特例化是不需要特例化而强制特例化。

如下,模板类的定义放到cpp中,这样会报错,因为在编译call_foo.cpp时不知到模板定义, 因为没有生成过int版本的Foo。
为此,必须在foo.cpp里添加template class Foo<int>, 如同4,实在吃力不讨好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// foo.h
template<typename T>
class Foo {
public:
Foo();
void someMethod(T x);
private:
T x;
};

// foo.cpp
template<typename T>
Foo<T>::Foo()
{
// ...
}
template<typename T>
void Foo<T>::someMethod(T x)
{
// ...
}

// call_foo.cpp
void blah_blah_blah()
{
// ...
Foo<int> f;
f.someMethod(5);
// ...
}

当然把模板的定义放到头文件中会增加可执行文件的体积。

实际运用

grpc 能以流和非流方式传输,而grpc参数protobuf消息类型是另一个范围。那么要封装grpc方法, 需要封装流+类型

举例protobuf的消息类型有string, fixed32

  • string
  • fixed32
  • Stream
  • Stream

通过上面的偏特化可以即能区分流和非流又能区分类型

赘述一下模板的类型

上面的模板类型T都是实际编程时定义的类型,但作为图灵完备的模板编程,未决的template类型也可以作为template类型

如下是一个模板用另一个模板来特例化

1
2
3
4
5
template <typename T> struct Upper {};
template <template <typename> class T> struct Lower {};

template<typename T>
Lower<Upper<T>> l;

可变模版variadic templates

c中有可变参数...和gcc内置__VA_ARGS__宏定义, 实现不同个数的变量打印。这是由编译期实现的,会将format的格式符替换成参数

1
int printf ( const char * format, ... );

c++有模版,而且在c++11之后引入了动态参数模版,即模版函数或类可以使用动态参数

实际运用1

在实际项目中手动跑单元测试用例的时候,不希望再去看日志文件,而是想日志直接输出到终端, 有以下办法

c++11及以上标准

可以使用动态参数模版替换原本的日志打印函数

1
2
3
4
5
6
7
8
9
#undef log_debug

template<typename First, typename ...Rest>
void log_debug(First && first, Rest && ... rest){
std::cout << fmt::format(first, rest ...) << std::endl;
}

// 这时日志就直接输出到终端了, 这里使用了fmt库
log_debug("aasdas{}", "bbbb");

也可简单写成

1
2
3
4
template<typename ...Args>
void log_debug(Args&& ...args){
std::cout << fmt::format(args...) << std::endl;
}

如果不使用fmt格式化,还可以用

1
2
3
4
5
6
7
8
9
#undef log_debug
// 定义一个空函数
void log_debug(){}

template<typename First, typename Rest>
void log_debug(First&& first, Arg&& ...arg){
std::cout << first;
log_debug(arg...);
}

需要解释一下,函数log_debug()必须要先声明,因为模版实例化的的最终要调用这个无参的函数
模拟一下堆栈, 因为参数在每次递归时减少一个,所以最终是0个参数

1
2
3
4
log_debug(1, 0.2, "aaa");
log_debug(0.2, "aaa");
log_debug("aaa");
log_debug();

c++11以前的标准

可以使用宏定义来替换了, 然后需要重载,方法

1
2
3
4
5
6
7
8
9
10
11
12
13
#define log_debug(...) std::cout , __VA_ARGS__ , std::endl

template <typename T>
std::ostream& operator,(std::ostream& out, const T& t) {
out << t;
return out;
}

//overloaded version to handle all those special std::endl and others...
std::ostream& operator,(std::ostream& out, std::ostream&(*f)(std::ostream&)) {
out << f;
return out;
}

直接用c的方式

因为printf是支持varidic的

1
2
3
4
5
6
7
8
#undef log_debug

#define log_debug(...) printf(__VA_ARGS__), printf("\n")

int main() {
log_debug("example","output","filler","text");
return 0;
}

c++17 引入了fold expression

可以改写为

1
2
3
4
5
template<typename ...Args>
void log_debug(Args && ...args)
{
(std::cout << ... << args);
}

动态参数模版除了以上的用法,还有更多用处, 例如std::tupe的实现

实际运用2

使用模板生成并发代码

以下代码实现复制二维数组,

1
2
3
4
5
for( size_t ch=0 ; ch<channelNum ; ++ch ) {
for( size_t i=0; i<length ; ++i ) {
out[ch][i]=in[ch][i];
}
}

但以下理论更快,没有两层for,前提是知道channel大小

1
2
3
4
5
6
for(size_t i=0;i<length;++i) {
out[0][i]=in[0][i];
out[1][i]=in[1][i];
out[2][i]=in[2][i];
out[3][i]=in[3][i];
}

但如果用模板,就不需要知道channel大小,自动生成上面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <int count> class Copy {
public:
static inline void go(float **const out, float **const in, int i) {
Copy<count - 1>::go(out, in, i);
out[count - 1][i] = in[count - 1][i];
}
};

template <> class Copy<0> {
public:
static inline void go(float **const, float **const, int) {}
};

template <int channelNum>
void parall_copy(float **out, float **in, size_t length) {
for (size_t i = 0; i < length; ++i) {
Copy<channelNum>::go(out, in, i);
}
}

需要提醒的,如同打印日志的,0的特例化不能省,否则编译出错

实际运用3 工厂模式

在使用spdlog时发现有使用到template未决名, 用来实现两个维度的工厂模式。

第一层提供两种sink的工厂,而其factory是未决名,所以要加上Factory::template消歧义,不然<会当成小于号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//
// factory functions
//
template<typename Factory = default_factory>
inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name, const filename_t &filename, bool truncate = false)
{
return Factory::template create<sinks::basic_file_sink_mt>(logger_name, filename, truncate);
}

template<typename Factory = default_factory>
inline std::shared_ptr<logger> basic_logger_st(const std::string &logger_name, const filename_t &filename, bool truncate = false)
{
return Factory::template create<sinks::basic_file_sink_st>(logger_name, filename, truncate);
}

此时factory可以是synchronous_factory, 也可以是异步版本,但这需要用户自己实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Default logger factory-  creates synchronous loggers
struct synchronous_factory
{
template<typename Sink, typename... SinkArgs>
static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&... args)
{
auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);
auto new_logger = std::make_shared<logger>(std::move(logger_name), std::move(sink));
details::registry::instance().initialize_logger(new_logger);
return new_logger;
}
};

using default_factory = synchronous_factory;

简化为下面的demo,固然可以直接使用call_dd的方式,但维度只有一个。而call_dd2则有2个维度了
但必须使用T::template消歧义, 因为此时的foo未决名, 不知道是那个类里面的foo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct AA {
template <typename cc> static void foo() { std::cout << "dd::foo\n"; };
};

struct BB {
template <typename cc> static void foo() { std::cout << "dd::foo\n"; };
};

template <typename T> void call_dd() { AA::foo<T>(); }
template <typename T, typename K> void call_dd2() { T::template foo<K>(); }

int main() {
call_dd<void>();
call_dd2<AA>();
call_dd2<AA, int>();
call_dd2<BB, long>();
}

##模版与宏定义、虚函数的区别

  1. 宏定义在预处理期执行,模板在编译期执行,而虚函数也称动态绑定在运行时执行
  2. 宏和模板都将运行时的工作提前了,用编译时间换取运行效率
  3. 宏定义没有类型检查,这点模板比较好
  4. 模板虽然会延长编译时间,但当编译期实例化类型后,查找模板函数和查找普通函数的速度几乎相同

待决名dependent name

  1. 待决名的意思是在定义的地方,类型还不能决断,需要延后到实例化确定时。而非待决名指类型在定义的地方已经确定。
  2. 延后将导致此时无法在定义点进行错误检查,以及消除typenametemplate歧义,这导致需要在调用点加上template

待决名如:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
struct X : B<T> // "B<T>" 取决于 T
{
typename T::A* pa; // "T::A" 取决于 T
// (此 "typename" 的使用的目的见下文)
void f(B<T>* pb)
{
static int i = B<T>::i; // "B<T>::i" 取决于 T
pb->j++; // "pb->j" 取决于 T
}
};

让人吃惊的例子, 这就是非待决名的情况下,立即绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

void g(double) { std::cout << "g(double)\n"; }

template<class T>
struct S
{
void f() const
{
g(1); // "g" 是非待决名,现在绑定
}
};

void g(int) { std::cout << "g(int)\n"; }

int main()
{
g(1); // 调用 g(int)

S<int> s;
s.f(); // 调用 g(double)
}

typename消歧义

在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是类型,
除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <vector>

int p = 1;

template<typename T>
void foo(const std::vector<T> &v)
{
// std::vector<T>::const_iterator 是待决名,
typename std::vector<T>::const_iterator it = v.begin();

// 下列内容因为没有 'typename' 而会被解析成
// 类型待决的成员变量 'const_iterator' 和某变量 'p' 的乘法。
// 因为在此处有一个可见的全局 'p',所以此模板定义能编译。
std::vector<T>::const_iterator* p;

typedef typename std::vector<T>::const_iterator iter_t;
iter_t * p2; // iter_t 是待决名,但已知它是类型名
}

template<typename T>
struct S
{
typedef int value_t; // 当前实例化的成员
void f()
{
S<T>::value_t n{}; // S<T> 待决,但不需要 'typename'
std::cout << n << '\n';
}
};

int main()
{
std::vector<int> v;
foo(v); // 模板实例化失败:类型 std::vector<int> 中没有
// 名字是 'const_iterator' 的成员变量
S<int>().f();
}

template消歧义

与此相似,模板定义中不是当前实例化的成员的待决名同样不被认为是模板名,除非使用消歧义关键词 template,或它已被设立为模板名:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
struct S
{
template<typename U> void foo() {}
};

template<typename T>
void bar()
{
S<T> s;
s.foo<T>(); // 错误:< 被解析为小于运算符
s.template foo<T>(); // OK
}

template 消歧义可以使用

1
2
3
T::template
s.template
this->template

std::forward 转发在模版的使用

为什么完美转发的对象必须是右值引用?

说明一下右值引用

1
2
3
4
5
6
 引用类型 	可以引用的值类型 	使用场景
非常量左值 常量左值 非常量右值 常量右值
非常量左值引用 Y N N N 无
常量左值引用 Y Y Y Y 常用于类中构建拷贝构造函数
非常量右值引用 N N Y N 移动语义、完美转发
常量右值引用 N N Y Y 无实际用途

##参考
https://en.cppreference.com/w/cpp/language/parameter_pack
https://en.cppreference.com/w/cpp/language/fold
https://en.cppreference.com/w/cpp/language/overload_resolution#Best_viable_function