跳转至

Welcome

前言

偶尔看到自己或者客户的/var/log/message 日志出现segfault, 查询了一下相关信息

Apr  6 09:43:37 icm kernel: rhsm-icon[13402]: segfault at 12b0000 ip 0000003c89845c00 sp 00007ffce18396e0 error 4 in libglib-2.0.so.0.2800.8[3c89800000+115000]

解释

  • address (after the at) - the location in memory the code is trying to access (it's likely that 10 and 11 are offsets from a pointer we expect to be set to a valid value but which is instead pointing to 0)
  • ip - instruction pointer, ie. where the code which is trying to do this lives
  • sp - stack pointer
  • error - An error code for page faults; see below for what this means on x86.
    /*
     * Page fault error code bits:
     *
     *   bit 0 ==    0: no page found       1: protection fault
     *   bit 1 ==    0: read access         1: write access
     *   bit 2 ==    0: kernel-mode access  1: user-mode access
     *   bit 3 ==                           1: use of reserved bit detected
     *   bit 4 ==                           1: fault was an instruction fetch
     */
    

message日志

使用dmesg打印ring buffer的内容,关于硬件和i/o的信息

coredump

 A core file is an image of a process that has crashed It contains all process information pertinent to debugging: contents of hardware registers, process status, and process data. Gdb will allow you use this file to determine where your program crashed. 

复现

void foo(){
    int *p = 0;
    *p = 100;
}

int main(){
  foo();
}
[ 5902.293905] a.out[6085]: segfault at 0 ip 000055c0eddca129 sp 00007ffe65372110 error 6 in a.out[55c0eddca000+1000]
[ 5902.293916] Code: 00 c3 66 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 40 00 f3 0f 1e fa e9 67 ff ff ff 55 48 89 e5 48 c7 45 f8 00 00 00 00 48 8b 45 f8 <c7> 00 64 00 00 00 90 5d c3 55 48 89 e5 b8 00 00 00 00 e8 d9 ff ff
(gdb) info registers 
rax            0x0                 0
rbx            0x55c0eddca150      94287112741200
rcx            0x7faa085eb598      140368261592472
rdx            0x7ffe65372228      140730596532776
rsi            0x7ffe65372218      140730596532760
rdi            0x1                 1
rbp            0x7ffe65372110      0x7ffe65372110
rsp            0x7ffe65372110      0x7ffe65372110
r8             0x0                 0
r9             0x7faa08621070      140368261812336
r10            0x69682ac           110527148
r11            0x202               514
r12            0x55c0eddca020      94287112740896
r13            0x0                 0
r14            0x0                 0
r15            0x0                 0
rip            0x55c0eddca129      0x55c0eddca129 <foo+16>
eflags         0x10246             [ PF ZF IF RF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0

addr2line

addr2line -e yourSegfaultingProgram 00007f9bebcca90d

cmake 学习

add_custom_command 用法

用来定义自定义的方法, 而且有2套签名或者说触发规则

add_custom_target 配合使用, 用于生成文件

这种情况下,add_custom_target 续要在add_custom_command之后出现。 语法

add_custom_command(OUTPUT output1 [output2 ...]
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [MAIN_DEPENDENCY depend]
                   [DEPENDS [depends...]]
                   [BYPRODUCTS [files...]]
                   [IMPLICIT_DEPENDS <lang1> depend1
                                    [<lang2> depend2] ...]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [DEPFILE depfile]
                   [JOB_POOL job_pool]
                   [VERBATIM] [APPEND] [USES_TERMINAL]
                   [COMMAND_EXPAND_LISTS])

类似make的语法规则

target:dependency
  command
如果dependency不存在,就去找生成依赖本身的规则,没有也生成依赖的规则,那么make会停止。

如下的例子

 cmake_minimum_required(VERSION 3.5)
 project(test)
 add_executable(${PROJECT_NAME} main.c)
 add_custom_command(OUTPUT printout 
                    COMMAND ${CMAKE_COMMAND} -E echo compile finish
                    VERBATIM
                   )
 add_custom_target(finish
                   DEPENDS printout
                   )

finish 依赖 printout, 而add_custom_command定义了printout的规则,printout即为下面的command执行的输出

所以当生成finish目标的时候会触发上面的add_custom_command

其实这种情况下, 直接将add_custome_command的command写到add_custome_target中也是一样的效果

command-line-tool

以上add_cunstom_command的两种用法都使用了COMMAND ${CMAKE_COMMAND} -E,这是使用了cmake内置的[命令]{https://cmake.org/cmake/help/latest/manual/cmake.1.html#run-a-command-line-tool}

运用

例如生成protobuf的文件, 需要自定义方法

    # output files:
    FOREACH (src ${proto_srcs})
        get_filename_component(base_name ${src} NAME_WE)
        get_filename_component(path_name ${src} PATH)

        set(src "${base_name}.proto")
        set(cpp "${base_name}.pb.cc")
        set(hpp "${base_name}.pb.h")
        set(grpc_cpp "${base_name}.grpc.pb.cc")
        set(grpc_hpp "${base_name}.grpc.pb.h")

        # custom command.
        add_custom_command(
            OUTPUT ${proto_cpp_dist}/${cpp} ${proto_cpp_dist}/${hpp} ${proto_hpp_dist}/${hpp}
              ${proto_cpp_dist}/${grpc_cpp} ${proto_cpp_dist}/${grpc_hpp}
            COMMAND ${PROTOBUF_PROTOC_EXECUTABLE}
            ARGS ${OUTPUT_PATH}
              --grpc_out ${proto_cpp_dist}
              --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN}
              ${src} 
            DEPENDS ${src}
            COMMAND ${CMAKE_COMMAND}
            ARGS -E copy_if_different ${proto_cpp_dist}/${hpp} ${proto_hpp_dist}/${hpp}
            COMMAND ${CMAKE_COMMAND}
            ARGS -E copy_if_different  ${proto_cpp_dist}/${grpc_hpp} ${proto_hpp_dist}/${grpc_hpp}
            WORKING_DIRECTORY ${path_name}
            COMMENT "${PROTOBUF_PROTOC_EXECUTABLE} --cpp_out=${proto_cpp_dist} ${src}"
            )

        LIST(APPEND output ${proto_cpp_dist}/${cpp})
    ENDFOREACH()

单独使用, 编译触发

这个是当项目中有add_library或者add_excutable目标时可以在编译目标文件前/链接前/编译后触发

add_custom_command(TARGET <target>
                   PRE_BUILD | PRE_LINK | POST_BUILD
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [BYPRODUCTS [files...]]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [VERBATIM] [USES_TERMINAL]
                   [COMMAND_EXPAND_LISTS])

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(), 然后可以在链接的时候使用第三方库

find_package()

Config模式

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

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

安装FindXXX.cmake文件

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

$ pacman -S extra-cmake-modules

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

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规则, 内容大致为

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,在其中添加目标,已备在项目中链接
    # 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之后,可以使用这个办法来自动拉取网上的库,并可以直接在自己的项目中使用

# 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文件, 导致项目配置失败

 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方式
#/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文件的路径

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'文件头

# - 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等

shell 笔记

shell 是unix-like系统下,用户与系统交互的媒介,用来解析用户的输入并调用系统函数。 而shell的实现有常见的bash,zsh,ksh等, 他们实现有很多差别,但bash最为通用

bash 模式拓展

bash 字符串操作

bash 数组操作

环境变量

测试程序定时获取和打印环境变量

int main() {
  while (1) {
    char *env = getenv("TEST_ENV");
    printf("env: %s\n", env);
    sleep(5);
  }
}

通过bash来修改环境变量

#test.sh
export TEST_ENV=TEST
./a.out
export TEST_ENV=NNN

执行test.sh,c程序没有更新环境变量, 所以环境变量不会变化。

env: TEST
env: TEST
env: TEST
#include <stdio.h>

extern char **environ;

int main() {
  char **var;
  for (var = environ; *var != NULL; ++var) {
    printf("%s\n", *var);
  }
}

set unset

前言

自使用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函数如下

# 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'

[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"

!!$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

前言

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

现象

../../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),而普通指针不是,举例如下

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>);

引用计数

shared_ptr.png

当形参为普通指针时

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

1. 为t1申请内存
2. 构造t1
3. 为t2申请内存
4. 构造t2
5. 调用函数

  • 当执行到2失败时,不会泄漏,new会释放t1的内存
  • 当执行到4失败是,会泄漏t1,因为t1已经构造完成,不会释放内存

当形参为智能指针时

步骤为

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. 为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, 可以使用模版实现如下

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析构时,自动关闭文件

// 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之后,原对象不拥有指针资源

总结

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

前言

自从知道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就可以访问两个网络

use-ipv4=yes
allow-interfaces=br0,wlan0
再设置自启动

nss 配置

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

- 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域名

~ > 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的配置目录下

sudo cp /usr/share/doc/avahi/ssh.service /etc/avahi/services
这样,别人就能发现我的ssh服务了

配置打印机

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

最后,配置无线功能

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

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

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

<?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服务

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

模板编程

模板编程是其它高级语言没有的技术, 也称为范型编程,元编程(meta programing), stl的基石。这种对类型的泛化相当于在编程之上编程

概述

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

 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个方向,类型特化范围特化

类型特化

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

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

例如`Foo foo',编译器会使用第三个版本

范围特化

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

template <typename T, typename N> struct Foo<T *, N> {};
这样'Foo foo'会使用这个版本

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

//模板
template<int n> foo(){}

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

函数匹配优先级

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

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),这样可以有自定义类型的特化版本

  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,实在吃力不讨好。

// 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类型

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

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的格式符替换成参数

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

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

实际运用1

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

c++11及以上标准

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

#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");

也可简单写成

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

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

#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个参数
log_debug(1, 0.2, "aaa");
log_debug(0.2, "aaa");
log_debug("aaa");
log_debug();

c++11以前的标准

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

#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的

#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

可以改写为

template<typename ...Args>
void log_debug(Args && ...args)
{
    (std::cout << ... << args);
}

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

实际运用2

使用模板生成并发代码

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

for( size_t ch=0 ; ch<channelNum ; ++ch ) {
    for( size_t i=0; i<length ; ++i ) {
        out[ch][i]=in[ch][i];
    }
}

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

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大小,自动生成上面的代码

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消歧义,不然<会当成小于号

//
// 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, 也可以是异步版本,但这需要用户自己实现。

// 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。

  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

待决名如:

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
    }
};

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

#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 声明或通过用作基类名)。

#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,或它已被设立为模板名:

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 消歧义可以使用
T::template
s.template
this->template

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

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

说明一下右值引用

 引用类型   可以引用的值类型    使用场景
非常量左值   常量左值    非常量右值   常量右值
非常量左值引用     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

临时变量

我们说的临时变量通常是指在语句块里定义的短暂生命周期的局部变量, 其存储周期为'auto',但这里讨论的是c++中'temporary object', 是在老版本c++中 而语句块里定义的变量以及非引用类型参数,并不属于这里的临时变量

c++中的临时变量出现情况: * litteral常量, 如1 * 类型转换 // 赋值语句结束后,自动销毁 * 函数返回值 // 赋值语句结束后,自动销毁 * 表达式的值

而临时变量的定义就是编译器自动创建和销毁的没有名字的变量, 也无法取地址。并且规定:
- 常量类型的引用(reference to const)可以绑定到临时变量 - 非常量类型的引用(reference to non-const)类型不能绑定到临时变量

这个规定的原因是: 因为上面说的编译器自动创建和销毁,所以去修改一个会销毁变量是没有意义的

什么时候编译器要需要创建临时变量

创建原因 销毁时机
Result of expression evaluation All temporaries created as a result of expression evaluation are destroyed at the end of the expression statement (that is, at the semicolon), or at the end of the controlling expressions for for, if, while, do, and switch statements.
Initializing const references If an initializer is not an l-value of the same type as the reference being initialized, a temporary of the underlying object type is created and initialized with the initialization expression. This temporary object is destroyed immediately after the reference object to which it is bound is destroyed.

主要是以下两个地方会需要临时变量: * 函数接受引用类型参数的时候 * 函数返回的时候 * 类型转换的时候

举例:

函数参数

void foo(const int &arg){}
foo(1);  // 此时创建了临时变量,可以想象成'foo(int _tmp(i));' 而且这个_tmp 不能被修改

函数返回值

int foo(){return 1;}
int i=foo(); // 此时创建了临时变量, int _tmp = foo(); int i = _tmp;

类型转换

int i = 1;
double d = i; // double _tmp = i; double d = _tmp;

单独讨论这些似乎意义不大, 但是在实际编写代码的时候,可能会有疑惑

例如如何解释

const int& cr = 1; // ok
int &r = 1; // ng
这里赋值和函数传参一样, 因为1是常数,这里为常量1创建临时变量, 而只有reference to const 才能绑定到临时变量上

还有解释一个经常被引用的例子

void foo(int &arg){}
int i = 1; foo(i); // ok
double d = 1.0; foo(d); // ng

void foo(const int &arg){}
int i = 1; foo(i); // ok
double d = 1.0; foo(d); // ok
这里虽然i和d都不是常量(不会创建临时变量), 但是因为类型不同, 发生了类型转换,创建了临时变量。同样地,只有reference to const 才能绑定到临时变量上