moka项目开发过程记录

总结记录开发过程等于二次开发

  • !!用法
  • 抽象基类里可以定义实现
  • 默认参数只在声明中出现
  • 重载最好用override关键字(若重写虚函数时函数名写错,编译器会报错),补充一下(final阻止进一步的派生和虚函数的进一步重写)。override关键字只能在类内使用
  • 纯虚函数可以实现
  • stringstream类对象,当需要按预定的格式将程序中的数据保存在一个string中的时候
  • stringstream.str()为返回string buffer的copy
  • #为宏的字符串操作符
  • std::get(i)用于获取tuple的第i个元素
  • 继承enable_shared_from_this<T>才能在成员函数中安全获得this的智能指针版本,通过shared_from_this()获得
  • stringstream通过<<将字符串放入缓冲区中(类似cout),.str()成员函数获取缓冲区中的数据
  • 时间用localtime先是从epoch到现在的时间UTC
  • C++11构造函数初始化列表的顺序和成员变量声明的顺序必须一致
  • 使用wrap类作为临时对象,使用完后直接析构,触发日志写入,然而日志本身的智能指针,如果声明在主函数里面,程序不结束就永远无法释放。利用MOKA_LOG_LEVEL宏在if语句内部构造了局部变量,if语句结束后会调用LogEventWrap在if语句结束后会结束生命周期调用析构函数实现打印
  • 日志器会初始化默认格式器,格式器会初始化items。日志事件的log方法中调用对应子类appender的log,appender中的日志格式器会调用format方法遍历items,利用多态,调用对应item的format输出格式对应的信息
  • __VA_ARGS__可变参数宏,将被替换为可变参数列表
  • 可变参数
    • va_list宏用于支持可变参数,声明的变量用于存储可变参数列表
    • va_start第一个参数为va_list,第二个参数为可变参数之前的一个参数(即最后一个固定参数)
    • va_arg可用于获取可变参数列表中指定类型的参数(每次调用va_arg,可变参数列表的指针都会向后移动(从头开始))。如va_arg(args, int)
    • va_end用于清理va_list变量
  • 若直接父类声明了有参构造函数,则子类构造函数要传入直接父类的参数,并显式调用直接父类的构造函数
  • std::exceptionwhat成员函数返回一个错误信息的解释性字符串
  • 可以通过typeid(变量).name()来获取对象的类型名称的字符串,与sizeof相比,typeid是在运行时求值
  • 选用boost库,在类型转换方面要方便,不需要stodstof
  • dynamic_pointer_cast返回会使引用计数+1,用于智能指针模板类型的转换
  • 模板内嵌套使用模板必须要用typename指出,告诉编译器typename后面的字符串为类型名称,而不是成员变量或者成员函数名称
  • std::transform(name.begin(), name.end(), name.begin(), ::tolower)将字符串转换成小写
  • static变量初始化时,要加上类名和作用域限定符(类型 类名::变量名)
  • 配置系统原则,约定优于配置
  • 模板全特化:模板参数列表没有参数,class 类名后限定死了类型,如template<> class X<int, int>
  • 模板偏特化(优先匹配更接近的版本):模板参数列表有参数,并未完全限定死类型,如template<class T> class X<int, T>
  • 使用stringstream可以简化数据类型的转换(可以将其它类型的值转换为字符串),.str("")可以设置一个新的字符串缓冲区,str()可获取缓冲区的内容。通过ss <<可以将其他类型转换为字符串,ss >>可以将字符串转换为其他类型
  • 局部静态变量也是一直存在的,可以一直被初始化,因此单例模式有一种Meyers实现方式,利用静态局部变量获取instance。C++11后,比那一起能保证局部静态变量的线程安全性
  • 全局变量静态变量在main函数之前初始化
  • 不同文件静态变量初始化顺序不一致,非局部的静态变量的初始化顺序是不确定的,解决方法是把它变成一个局部静态变量
  • ConfigVar模板类在实例化时,都要提供其FromStr和ToStr两个仿函数(在类中实现operator()来实现),复杂类型要进行对应的偏特化(对LexicalCast)
    1
    2
    template <class T, FromStr = LexicalCast<std::string, T>, ToStr = LexicalCast<T, std::string>>
    class ConfigVar
  • 类模板调用优先级:全特化类 > 偏特化类 > 主版本模板类
  • 调用LoadYaml之后LogDefine结构体发生更改,然后会调用这个LogIniter注册的回调函数,对logger日志器类的属性值进行更改
  • thread local关键字修饰的变量,表示每一个线程都拥有这个变量
  • pthread_self获取的是pthread库对应的线程号(top命令是查不到的),而不是linux系统对应的编号
  • volatile防止编译器优化,每次都从内存中取数据
  • 继承std::enable_shared_from_this<T>类,调用shared_from_this方法可以获取到当前类的智能指针
  • 协程调度器:将协程指定到相应的线程上去执行
  • LogEventWrap日志事件包装类,方便使用宏进行打log,在LogEventWrap的析构函数中打印Log,因为它在宏中为局部变量
  • 使用配置变更事件机制,可以方便配置日志,因为与yaml相关的日志信息使用LogDefine存储,修改配置项的值时会触发在main函数之前创建的logInit回调函数,在其中将变化的logDefine结构体的信息(增/删/改)更新到Logger类中
  • 智能指针进行初始化时,可以用std::shared_ptr::swap来进行赋值,两个操作数的引用计数不会改变。swap之后两个智能指针对象swap了一下,所以引用计数也swap了。所以在对智能指针进行赋值时可以使用std::move本质上和swap是一样的
  • 智能指针的指针传递时并不会增加引用计数
  • 经过检验,使用构造函数初始化列表传智能指针值进行赋值,引用计数会从1变成3,但最后会释放掉1个也就是初始化列表的那个。因此按值传参后可以reset一下,让引用计数减1
  • std::bind绑定器返回类型是一个stl内部定义的仿函数类型,可以绑定函数的入口地址和参数,并可以使用std::function进行保存
  • 原子类型只保证线程安全,不保证线程同步?
  • stderror(errno)将error number用字符串表示出来
  • 智能指针遇到的问题
    1
    2
    3
    4
    5
    6
    7
    // GetThis后会返回当前协程的this的智能指针,这里yield之后,无法继续执行该函数体,故cur无法进行释放(没有执行出作用范围,即函数体,无法进行释放),所以cur需要显式子进行释放
    {
    Fiber::ptr cur = Fiber::GetThis();
    auto raw_ptr = cur.get();
    cur.reset();
    raw_ptr->yield();
    }
  • 扩容为fd的值*1.5,因为fd的值可能很大超过fd上下文集合的大小
  • 在iomanager中调用schedule执行的notify、stop、idle,根据虚函数原理,都会执行IOManager::的相应的虚函数版本
  • 若不使用caller线程,则在run函数中新建主协程为调度协程
  • IOManager中的idle通过back回到事件循环之后,会直接跳过MainFunc中设置协程状态为TERM的过程,因此到事件循环后会被设置为HOLD。run中事件循环中重新启动后,HOLD状态的协程不会被放入事件队列,因此此时满足stop的条件,调度器就停止了
  • IOManager中Addevent对某个fd注册的事件,会在epoll_wait返回之后,如果有addEvent注册的事件则调用回调函数
  • IOManager析构时会调用stop
  • idle事件循环中,在处理fd监听到的事件时,一个线程处理结束后会修改epoll内核事件表,然后下一个线程就不处理该事件了
  • 实现一个定时器,利用epoll_wait的第四个参数(毫秒级),进行整合
  • 友元类有当前类的访问权限
  • 使用set进行自定义对象排序时,比较的时候得重载比较运算符
  • set.insert()返回一个pair,<插入位置的迭代器,是否成功的bool变量>
  • weak_ptr中lock方法能够返回当前weak_ptr对应的shared_ptr的对象,如果有则获取到shared_ptr对象,同时引用计数+1,如果没有则返回nullptr
  • std容器中,lower_bound返回的是第一个大于等于该值的迭代器的位置
  • set中自定义比较器类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <set>
    #include <iostream>

    struct MyCompare {
    bool operator()(const int& a, const int& b) const {
    // 按照元素的绝对值大小进行比较
    return std::abs(a) < std::abs(b);
    }
    };

    int main() {
    std::set<int, MyCompare> s = {4, -2, 5, -3, 1};
    for (auto it = s.begin(); it != s.end(); ++it) {
    std::cout << *it << ' ';
    }
    std::cout << std::endl;
    return 0;
    }
  • std::function用内部可能使用智能指针来管理,如果可调用对象是一个函数对象或lambda表达式,就是new运算符在堆上动态分配一个函数对象;如果可调用对象是一个函数指针,就直接将其存储在函数指针成员中。
  • 定时器采用最小堆来实现,根据超时时间点进行排序,超时时间到了之后,获取当前的绝对时间点,然后把最小堆里超时时间点小于这个时间点的定时器收集起来(这些定时器都超时了),执行他们的回调函数
  • 如果系统时间发生了调整,则触发全部定时器
  • hook模块,可以使一些不具备异步功能API展现出异步的性能。hook和IO协程调度联系起来之后,因为同一个线程中的协程是串行执行的,当处于同一个线程时,一个协程阻塞以后,HOOK模块可以使得该协程切换到另一个协程执行。因此hook是为了保证整个线程不会因为一个协程阻塞了而阻塞。(配合定时器)
  • 配合定时器模块,可以实现单个线程中的协程异步执行
  • 基于动态链接的hook有两种方式,第一种是外挂式hook(i.e.非侵入式hook),通过优先加载自定义的动态库来hook后加载的动态库,这种方式不需要重新编译代码
  • hook,修改系统函数的实现行为
  • c语言不支持重载
  • 宏中##作为连字符
  • 全局静态变量会在main函数之前初始化
  • 两个文件中使用同一变量时要用extern显子声明外部链接
  • 获取动态链接符号表中库函数的地址,并在hook.cc中重写该库函数,并封装起来
  • bool is_init_: 1告诉编译器只为该变量分配1个bit
  • C++11类内初始化后,可不用构造函数初始化
  • READY状态的协程会被调度器自动重新调度,而HOLD状态的协程需要显式地再次将协程加入调度
  • 当使用main线程作为调度线程时,需要新建调度协程,此时的调度协程是需要栈空间的(因为需要运行run函数);新建线程作为调度线程时,调度协程则不需要栈空间(因为此时run函数由新建的线程来运行),这时候线程的主协程就是调度协程。不过主协程自始自终都不需要分配栈空间,调度协程的栈空间分配视情况而定。
  • 如果使用caller线程作为调度线程,则在Fiber构造函数中,初始化主协程的同时会初始化调度协程(分配栈资源,执行run函数)
  • epoll_wait如果在超时时间内有事件发生,则立即返回。
  • 文件描述符上下文记录着epoll监听的事件集合,以及读写事件上下文(包括回调函数、任务协程、调度器指针等)。通过epoll.data存储文件描述符上下文指针。
  • cancel和delEvent的区别在于,cancel在从epoll事件表中删除指定事件后会触发(调度执行)该事件的回调函数或者任务协程
  • 个人觉得之所以要自定义一个fd的上下文,是为了在事件发生时让调度器去调度该回调函数或者任务协程,这样就能支持任务为协程或者回调函数了。
  • 当一个文件描述符关闭时,相应的 epoll 事件表会将该文件描述符从事件队列中删除,以确保不再监听该文件描述符的事件。这种机制可以防止已关闭的文件描述符继续产生事件,从而导致 epoll 实例的无效阻塞和性能下降。同时,该机制还可以确保 epoll 事件表的正确性和一致性,从而保证 epoll 实例的稳定和可靠性。
  • 注意close只能在最后一个回调函数体中调用(视情况而定),否则如果在addEvent事件体中进行调用,就会出现close了,此时也从fd相关的事件也从epoll内核事件表中删除。但这时候由于addEvent,pending的数量并没有减少,因此就一直阻塞在idle状态
  • 循环定时,即这个定时器已经超时了,如果是循环定时,再重新放入到定时器堆中
  • 定时器管理器的ticked变量,避免了还没有更新epoll的定时时间,就触发了onTimerInsertAtFirst,因此在get_expire时才将ticked置为false(避免频繁触发)
  • gettimeofday不是系统调用,是在用户态实现的
  • 如果系统时间被调整到一个小时之前,就触发所有定时器,执行所有定时器的回调函数。
  • 抽象基类(存在纯虚函数的类)不能实例化
  • 调用 sleep() 函数会使当前线程进入睡眠状态(也称为等待状态),并暂停其执行。在睡眠状态下,线程是暂停执行的,但仍然保持在运行队列中,并继续占用系统资源,例如内存。
  • 在hook.cc中声明外部变量一定不要放到moka的命名空间中,否则链接时找不到
  • fdmanager为了区分具体是文件还是socket,维护fd的状态信息
  • 不能嵌套使用成员中的锁
  • fstat函数用于获取文件描述符对应的元数据,S_ISSOCK(stat.st_mode)返回当前文件描述符是否为socket
  • 使用线程局部变量,不同线程的局部变量之间互不干扰(一个线程的局部变量如协程上下文,可以和另一个线程的上下文不一样)
  • 在使用模板函数时,编译器会根据模板实参的类型,自动推导出模板函数的类型参数。模板函数声明时使用一个或多个类型参数,这些类型参数可以在函数体中使用。
  • errno 11Resource temporarily unavailable表示资源暂时不可用,比如使用非阻塞socket,accept没有任何请求需要等待就会返回11。operation now in progress表示非阻塞套接字正在进行连接操作
  • std::forward完美转发(模板函数)可以保留参数左值和右值属性,和参数的是生命周期正确(直接将参数传递给另一个函数,可能会丢失参数的左值或右值属性),主要是为了保证参数的左值或右值属性保留(完美转发)。如果没有使用完美转发,右值引用会被强制转换为左值引用
  • hook中do_io使用函数模板可变参数,保证匹配不同的IO系统调用
    1
    2
    3
    4
    template<typename... Args>
    void foo(Args... args) {
    // ...
    }
  • 在hook住socket之后,新建sockfd时会将fd的上下文信息加入到fd信息集合中,因此通过get返回为空,说明该fd未hook(个人认为,那么fdctx中is_socket有用吗?)
    • 也可能不是socketfd,如果是一般的文件描述符则需要手动fd上下文集合
    • 所以结论是fdctx记录的是参与hook后操作的fd
  • 切记范围锁在unlock时要保证上锁才进行释放,否则也会出现线程安全问题
  • 对fd可以设置超时属性,如果在读写该fd时,超过时间未读写到数据则返回-1。
  • do_io逻辑,考虑到超时,可以添加定时器,若不超时hook,则通过再次添加读写事件的方式来达到异步的效果。epoll_wait无论是超时事件还是读写事件都会触发返回,然后listallcbs可以读取所有的超时定时器的回调函数,并一一进行调度执行。
  • 如果read超时了,设置超时时间resume执行,不可能会再次超时
  • 不是套接字不需要hook(没必要)
  • 如果读锁人数为0,再unlock就会设置为读的最大值
  • INADDR_ANY表示Ipv4的地址,表示服务器可以接受来自任何网络接口的连接请求。该宏表示(0.0.0.0)通配地址
  • 在io协程调度中,idle函数内,epoll被唤醒之后,就会从idle协程中yield到调度协程执行,当然如果如果有定时事件或者是读写事件存在,调度器是不会停止的
  • 普通协程调度器不能使用hook之后的版本,因为TimerManager并未初始化(IOManager继承了这个类,因此会在创建IO协程调度器的时候初始化它),因此在addTimer内操作TimerManager变量时会出现segFault。
  • 如果是循环定时,则在listExpirecbs取出取出超时定时器事件的过程中,就再作为新的定时器插入到定时器堆中。
  • 定时器堆使用的key是定时器指针,先用定时器的绝对到期时间比较,如果到期时间相等再比较地址。
  • 当一个子类实现了其父类中的纯虚函数时,父类可以通过该纯虚函数调用子类实现的版本。如果父类有多个子类,那么调用哪一个子类版本的虚函数实现,取决于将哪一个子类对象的引用或指针赋给父类对象。如果是普通虚函数,要想调用父类版本的虚函数需要加类作用域限定直接调用,如father::virtualfunc()
  • memcmp如果比较字符,就会根据字典序来比较。具体用法memcmp(str1, str2, len),len为比较的字节长度
  • 有多个网卡,如何根据网卡地址去选择IP?
  • 在使用动态库时,位置无关的代码(编译时加上-fpic)非常重要,因为动态库可以在内存中的任何位置加载和执行
  • add_library(moka SHARED ${LIB_SRC})将源文件编译成目标文件,生成动态库。在应用程序使用动态库时,不需要全部加载(只需要加载需要的实例即可),不需要将整个动态库链接到应用程序中。
  • char型数组和使用char*定义的字符串不会自动在末尾添加字符串结束符;但是const char*在末尾自动加结束符
  • Ip地址由(网络地址和主机地址构成)。CIDR表示法前缀一般称为网络地址的bit数
  • 纯虚函数类(抽象基类),指的所有虚函数都是纯虚函数。如果直接基类不重写这些纯虚函数,那么它也将作为抽象基类
  • 在 C++ 中,如果在不同文件中定义了同名的静态变量,每个文件中的静态变量都是独立的,它们具有各自的存储空间和作用域。静态变量的作用域限定在定义它的文件中,其他文件无法直接访问该变量。每个文件中的静态变量在程序运行时会分别存在内存中,它们互不干扰。
  • 在有序容器中查找某个重复元素的范围可以用std::equal_range,会返回一个std::pair类型
  • socket中的protocol参数为0,表示默认follow地址族的设置
  • TLV(Type-Length-Value)协议,若字节最高位为1表示后续字节也属于该类型数据,最高位为0表示该字节为最后一个字节,将无符号整形数据拆分为多个7位字节进行编码,有符号整型需要转换为无符号,zigzag编码,将有符号整型转换为无符号整型数据进行VLE编码,然后将最高位的符号位翻转
  • 固定长度需要考虑字节序,可变长度不需要

项目开发遇到的一些BUG

  • 只能在调度器创建以后添加任务(因为协程载体的局限),因为调度器在构造时,才会初始化静态局部变量(use_caller时是在构造函数中初始化,非use_caller只有在run时才是初始化),当前调度器的调度协程,因此,Fiber初始化时返回link需要保证该变量是初始化的,这是个很严重的BUG。因此如果自己测试使用Fiber来作为任务,就会出现bug,之前没有崩是因为之前测试基本上都是函数,函数的Fiber载体都是在run函数中,在t_sched_fiber初始化之后才创建的,因此不会出现问题。
    • 解决方案:在衍生一个MainFunc的版本(因为调用MainFunc时,肯定已经执行run函数了,即调调度协程已经被初始化),在Mainfunc里初始化返回的link,因为刚好mainfunc也是Fiber的成员函数,因此使用私有成员是没有问题的。故sched就调用MainFunc,call就调用MainFuncSched。(很遗憾MainFunc是static的,不过可以传入this指针)。
    • 这个BUG解决之后就可以在调度器之前创建任务Fiber执行了
    • 问题的原有之一,因为即便先start,也不能保证Fiber构造函数先于run函数中对t_sched的执行
    • 解决方案不可行的原因:因为已经调用MainFunc函数了,这时候再link已经晚了,因为一般函数调用之前都会先将返回地址先push。
    • 经过测验,协程作为任务添加是有BUG的,但函数作为任务添加不会出现
    • 已经修复,通过再run函数中再新建一个Fiber作为载体,这个暂存协程任务的fiber在run中初始化,就可以正常link了。
    • 最后不考虑uc_link了,直接区分不同版本的MainFunc,使用back或者yield返回即可,这里也要注意智能指针的处理

疑问

  • 为什么IO多路复用要搭配非阻塞IO?

调试Skill

  • ps aux | grep {string}查看名为string的线程信息,获取到进程id后
  • top -Hp {proces id}显示进程所包含的线程信息
  • gdb attch {proces id}调试已经在运行的进程(方便判断死锁)
  • 多线程调试
    • info thread查看所有线程
    • thread id切换到指定id的线程
    • set scheduler-locking on|off锁定当前线程,即只在当前线程执行
  • gcc指定链接库-L<路径名> -l<库名> -I<头文件路径>,注意库名不带lib前缀和.so后缀
  • 如果使用动态库找不到路径可以通过export LD_LIBRARY_PATH=/home/moksha/moka/lib命令设置环境变量

常用工具

netstat
  • -l显示所有listenning状态的端口
  • -t显示所有监听TCP的端口
  • -p显示PID和程序名称
  • -n以数字的形式显示地址,而不是尝试用主机或端口名
  • -a显示所有端口
man手册
  • 1.用户命令(User Commands)
  • 2.系统调用(System Calls)
  • 3.库函数(C Library Functions)

协程的优势

  • 减少了线程切换的成本(尤其是在高并发时,协程切换的效率高),适合于I/O密集型场景
  • 协程不存在并发问题
  • 协程更轻量,栈更小

协程调度

  • 每个线程同一时间内只能有一个协程在执行
  • 每个线程都有个主协程,主协程可以创建协程并调度协程,子协程只能回到主协程(非对称协程调度)
  • 一个线程同一时刻只能运行一个协程
  • 调度协程负责从调度器的任务队列中取任务执行,取出的任务即子协程,这里发生的主协程到子协程的切换为非对称协程。如果任务队列为空,调度协程会切换到idle协程,idle协程什么也不做
  • 在执行调度任务时,还可以通过调度器的GetThis()方法获取到当前调度器,再通过schedule方法继续添加新的任务,这就变相实现了在子协程中创建并运行新的子协程的功能。
  • main函数要等待调度线程结束后再退出
  • 在非caller线程中,调度协程就是调度线程的主协程;在caller线程中,调度协程并不是caller线程的主协程(如main),相当于caller线程的子协程。main函数线程有三类协程:main函数对应的主协程,调度协程,待调度的任务协程
  • 因此在初始化调度器时,use_caller表示使用当前调用者线程(调用构造函数的线程)来进行调度,caller线程会新建一个调度子协程来进行调度(注意这里执行调度的时机与执行start方法不在同一个地方)。如果是非caller线程,调度协程就是调度线程的主协程
  • 在main函数线程里这三类协程(为什么有3类呢,因为main线程的主协程要负责添加任务schedule())运行的顺序是这样的:
    1. main函数主协程运行,创建调度器
    2. 仍然是main函数主协程运行,向调度器添加一些调度任务
    3. 开始协程调度,main函数主协程让出执行权,切换到调度协程,调度协程从任务队列里按顺序执行所有的任务
    4. 每次执行一个任务,调度协程都要让出执行权,再切到该任务的协程里去执行,任务执行结束后,还要再切回调度协程,继续下一个任务的调度
    5. 所有任务都执行完后,调度协程还要让出执行权并切回main函数主协程,以保证程序能顺利结束。
  • 上面的case与非对称协程调度相悖?在caller线程中,调度子协程和任务子协程之间发生切换,怎么会到main主协程中?线程局部变量保存协程上下文,每个线程保存三个协程的上下文(正在执行的协程,调度协程,线程主协程)
  • 调度器所在的线程称为caller线程(即调度线程)
  • 若当前任务队列中没有任务执行才执行idle
  • caller线程作为调度线程处理相对于非caller来说是比较复杂的,所幸每个线程都有这样几个线程局部变量来保存协程上下文:1.当前正在执行的协程,2.当前线程的主协程,3.当前线程的调度协程。额外有一个当前线程的调度器
  • 任务协程执行过程中主动调用yield让出了执行权,调度器要怎么处理?再放进任务队列中
  • 子协程(调度协程/任务协程),只有在其协程对象调用sched其中调用swapcontext时才会调用初始化Fiber对象时用makecontext设置的MainFunc(其中执行run函数)发生调度
  • 有两种调度case:1. 使用caller线程作为调度线程(即main线程) 2. 使用新建的线程作为调度线程

ucontext

  • int getcontext(ucontext_t *ucp);使用当前执行的线程的上下文初始化协程上下文ucp
  • int setcontext(const ucontext_t *);恢复用户ucp的上下文
  • void makecontext(ucontext_t *ucp, (void *func)(), int argc, ...);,当使用setcontext或者swapcontext恢复ucp上下文时,调用func函数
  • int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);交换用户上下文

CMake语法

  • project(demo)会引入两个变量PROJECT_BINARY_DIRPROJECT_SOURCE_DIR
  • add_executable(demo demo.cpp)生成可执行文件
  • add_library(STATIC util.cpp)生成静态库
  • add_library(SHARED util.cpp)生成动态库
  • set(SRC_LIST main.cpp test.cpp)直接设置变量的值,$SRC_LIST就表示main.cpp和test.cpp
  • target_link_libraries(Demo MathFunctions)添加链接库,表明demo需要链接一个MathFunctions的库
  • 常用变量
    • EXECUTABLE_OUTPUT_PATH:重新定义目标二进制可执行文件的存放位置
    • LIBRARY_OUTPUT_PATH:重新定义目标链接库文件的存放位置
    • PROJECT_SOURCE_DIR:工程的根目录

yaml语法

  • 大小写敏感,使用缩进(缩进只允许使用空格)表示层级关系,缩进的空格数量没有限制(相同层级的元素左对齐即可),#表示注释
  • 数据类型
    • 对象(使用:结构表示key/value):键值对的集合
    • 数组(以-开 头)
    • 标量(scalars):不可再分的值
  • 类型值
    • enum value {undefined, NULL, Scalar, Sequence, Map};
  • 例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # logs中包含两个数组,两个数组元素中又包含字典name、level、formatter
    logs:
    - name: root
    level: info
    formatter: '%d%T%m%n'
    appender: # appender字典中包含两个数组(第一个数组中包含两个字典,第二个数组元素中包含一个字典)
    - type: FileLogAppender
    file: log.txt
    - type: StdoutLogAppender
    - name: system
    level: debug
    formatter: '%d%T%m%n'
    appender:
    - type: FileLogAppender
    path: log.txt
    - type: StdoutLogAppender

    # system为map,其元素也为map
    system:
    port: 9900 # 9000为scalar
    value: 15