不落辰

知不可乎骤得,托遗响于悲风

0%

bug及解决

日志

  • 起因:参考《Linux多线程服务端编程》第8章写my_muduo的class EventLoopThread时,想通过timerfd定时器测试功能,结果死循环。

    • 注册回调:在main中给EventLoopThread注册回调函数,在回调函数中创建channel以及事件。注册的回调函数会在EventLoopThread startLoop前调用。
  • 两处罪魁祸首

  • 问题发生处

  • 因此会与预期不符。

  • 可以看到 注册到epoll上的是timerfd=5,实际上返回的events.data.ptr指向的channel却是fd=0。

  • 日志打到这里时怀疑是使用了一个已经释放的对象,当时以为会是移动造成,又或者是使用了已经free的堆造成的。

  • 但其实还是不能肯定是内存方面的问题。因为没core dump.很奇怪为啥没core dump?

ASAN

  • 参考资料配置AddressSanitizer
  • 很强。一下子确定了在何处发生了什么错误。如下,在channel->handleEvent()时,发生stack-use-after-scope。比吭哧吭哧打日志方便很多。
    • 并且还可以编译时warning是否有初始化顺序不恰当的代码,比如类的成员在构造函数中初始化与在类中的声明顺序不一致。
  • 如此,可以确定时channel指针引用的对象已经不存在,并且是stack-use-after-scope而非heap-use-after-free。问题范围缩小一大半,直接去找定义该channel的地方。发现是在注册的回调函数中定义。因此,该channel出了回调函数就会被析构。(之前误以为注册给EventLoopThread的回调函数在其内部调用会展开,故认为loop结束前channel不会被析构,槽点过多)。
  • 更改channel定义的位置,使其不会在loop结束前被析构即可。
  • 也不是什么难bug,就是之前对这个内存方面的错误有些阴影,又恰好了解了asan这么好用的工具,因此在这里记录下。
    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
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    [INFO] 2022-09-20-12-30-33 : /home/shc/Muduo/src/main.cpp (64) <main>   startloop return eventloop 0x7f329bafdc40 

    [INFO] 2022-09-20-12-30-38 : /home/shc/Muduo/src/EpollPoller.cpp (147) <poll> 1 events happneded

    [INFO] 2022-09-20-12-30-38 : /home/shc/Muduo/src/EpollPoller.cpp (180) <fillActiveChannels> fill 5

    [INFO] 2022-09-20-12-30-38 : /home/shc/Muduo/src/EventLoop.cpp (109) <loop> 1 IN HANDLING 0

    =================================================================
    # error :stack-use-after-scope
    ==14905==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7f329bafd1d8 at pc 0x55840a7a315c bp 0x7f329bafba70 sp 0x7f329bafba60
    READ of size 1 at 0x7f329bafd1d8 thread T1
    # 显示过程调用栈 自出现问题处回溯一个个函数
    #0 0x55840a7a315b in Channel::handleEvent(Timestamp const&) /home/shc/Muduo/src/Channel.cpp:44
    #1 0x55840a7b2707 in EventLoop::loop() /home/shc/Muduo/src/EventLoop.cpp:112
    #2 0x55840a7bab27 in EventLoopThread::threadFunc() /home/shc/Muduo/src/EventLoopThread.cpp:93
    #3 0x55840a7bc5f3 in void std::__invoke_impl<void, void (EventLoopThread::*&)(), EventLoopThread*&>(std::__invoke_memfun_deref, void (EventLoopThread::*&)(), EventLoopThread*&) /usr/include/c++/7/bits/invoke.h:73
    #4 0x55840a7bc450 in std::__invoke_result<void (EventLoopThread::*&)(), EventLoopThread*&>::type std::__invoke<void (EventLoopThread::*&)(), EventLoopThread*&>(void (EventLoopThread::*&)(), EventLoopThread*&) /usr/include/c++/7/bits/invoke.h:95
    #5 0x55840a7bc2e9 in void std::_Bind<void (EventLoopThread::*(EventLoopThread*))()>::__call<void, , 0ul>(std::tuple<>&&, std::_Index_tuple<0ul>) /usr/include/c++/7/functional:467
    #6 0x55840a7bbf99 in void std::_Bind<void (EventLoopThread::*(EventLoopThread*))()>::operator()<, void>() /usr/include/c++/7/functional:551
    #7 0x55840a7bb9a2 in std::_Function_handler<void (), std::_Bind<void (EventLoopThread::*(EventLoopThread*))()> >::_M_invoke(std::_Any_data const&) /usr/include/c++/7/bits/std_function.h:316
    #8 0x55840a7a483b in std::function<void ()>::operator()() const /usr/include/c++/7/bits/std_function.h:706
    #9 0x55840a7c27c2 in operator() /home/shc/Muduo/src/Thread.cpp:63
    #10 0x55840a7c2f78 in __invoke_impl<void, Thread::start()::<lambda()> > /usr/include/c++/7/bits/invoke.h:60
    #11 0x55840a7c2bc2 in __invoke<Thread::start()::<lambda()> > /usr/include/c++/7/bits/invoke.h:95
    #12 0x55840a7c32e5 in _M_invoke<0> /usr/include/c++/7/thread:234
    #13 0x55840a7c326b in operator() /usr/include/c++/7/thread:243
    #14 0x55840a7c31cf in _M_run /usr/include/c++/7/thread:186
    #15 0x7f329f47e4bf (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xd44bf)
    #16 0x7f329f7be6da in start_thread (/lib/x86_64-linux-gnu/libpthread.so.0+0x76da)
    #17 0x7f329eec261e in __clone (/lib/x86_64-linux-gnu/libc.so.6+0x12161e)
    # 在哪里发生错误
    Address 0x7f329bafd1d8 is located in stack of thread T1 at offset 5720 in frame
    #0 0x55840a7b189d in EventLoop::loop() /home/shc/Muduo/src/EventLoop.cpp:81

    This frame has 26 object(s):
    [32, 33) '<unknown>'
    [96, 97) '<unknown>'
    [160, 161) '<unknown>'
    [224, 225) '<unknown>'
    [288, 289) '<unknown>'
    [352, 353) '<unknown>'
    [416, 417) '<unknown>'
    [480, 481) '<unknown>'
    [544, 552) '__for_begin'
    [608, 616) '__for_end'
    [672, 704) '<unknown>'
    [736, 768) '<unknown>'
    [800, 832) '<unknown>'
    [864, 896) '<unknown>'
    [928, 960) '<unknown>'
    [992, 1024) '<unknown>'
    [1056, 1088) '<unknown>'
    [1120, 1152) '<unknown>'
    [1184, 1696) 'header'
    [1728, 2240) 'header'
    [2272, 2784) 'header'
    [2816, 3328) 'header'
    [3360, 4384) 'buf'
    [4416, 5440) 'buf'
    [5472, 6496) 'buf' <== Memory access at offset 5720 is inside this variable
    [6528, 7552) 'buf'
    HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
    (longjmp and C++ exceptions *are* supported)

    # Thred T1在何处由T0生成
    Thread T1 created by T0 here:
    #0 0x7f329fa0dd2f in __interceptor_pthread_create (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x37d2f)
    #1 0x7f329f47e765 in std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xd4765)
    #2 0x55840a7c2931 in Thread::start() /home/shc/Muduo/src/Thread.cpp:64
    #3 0x55840a7ba49a in EventLoopThread::startLoop() /home/shc/Muduo/src/EventLoopThread.cpp:53
    #4 0x55840a7c5b0a in main /home/shc/Muduo/src/main.cpp:62
    #5 0x7f329edc2c86 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21c86)

    # 总结:handleEvent时 发生 stack-use-after-scope
    SUMMARY: AddressSanitizer: stack-use-after-scope /home/shc/Muduo/src/Channel.cpp:44 in Channel::handleEvent(Timestamp const&)
    Shadow bytes around the buggy address:
    0x0fe6d37579e0: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d37579f0: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d3757a00: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d3757a10: f8 f8 f8 f8 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8
    0x0fe6d3757a20: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    =>0x0fe6d3757a30: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8[f8]f8 f8 f8 f8
    0x0fe6d3757a40: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d3757a50: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d3757a60: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d3757a70: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    0x0fe6d3757a80: f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8 f8
    Shadow byte legend (one shadow byte represents 8 application bytes):
    Addressable: 00
    Partially addressable: 01 02 03 04 05 06 07
    Heap left redzone: fa
    Freed heap region: fd
    Stack left redzone: f1
    Stack mid redzone: f2
    Stack right redzone: f3
    Stack after return: f5
    Stack use after scope: f8
    Global redzone: f9
    Global init order: f6
    Poisoned by user: f7
    Container overflow: fc
    Array cookie: ac
    Intra object redzone: bb
    ASan internal: fe
    Left alloca redzone: ca
    Right alloca redzone: cb
    ==14905==ABORTING

ASan Address Sanitizer

概述

  • ASan 是 Address Sanitizer 简称,它是一种基于编译器用于快速检测原生代码中内存错误的工具。
  • 简而言之,ASan 就是一个用于快速检测内存错误的工具,目前已经集成在LLVM 3.1+和GCC 4.8+中
  • 可检测类型
  • 原理
    • 在编译时,ASan会替换malloc/free接口
    • 在程序申请内存时,ASan会额外分配一部分内存来标识该内存的状态
    • 在程序使用内存时,ASan会额外进行判断,确认该内存是否可以被访问,并在访问异常时输出错误信息

设置

  • CMakeLists.txt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 编译设置
    add_compile_options(-O0 -ggdb -std=c++14 -Wall -Wextra -mavx2
    -fsanitize=address
    -fno-omit-frame-pointer
    -fno-optimize-sibling-calls
    -fsanitize-address-use-after-scope
    -fsanitize-recover=address)
    # 链接设置
    target_link_libraries(muduo pthread -fsanitize=address)
  • 环境变量

    • llvm-symbolizer运行路径:export ASAN_SYMBOLIZER_PATH=”/usr/bin/llvm-symbolizer”
    • ASAN_OPTIONS为ASan运行的Flags:export ASAN_OPTIONS=”halt_on_error=0:log_path=xxx/asan.log:detect_stack_use_after_return=1”

注意

  • ASan版本程序在Linux环境下运行时会额外申请20TB的虚拟内存
    • 需要确保/proc/sys/vm/overcommit_memory的值不为2
    • 这也可以作为检验ASan是否工作的标志
  • ASan版本性能大幅受损,大约会下降2x左右
  • ASan工具不是万能的,他必须要跑到有问题的代码才可以暴漏出来

调试可执行文件

  • 编译项目时添加-g选项,二进制会带有调试信息

  • gdb + 二进制名字 开始调试程序

  • 打断点

    • break main:就是在要调试的程序入口设置断点
    • break 文件名:行号 chatservice.cpp:91
    • break 类名称::方法名 ChatService::login
  • 启动调试

    • start:从main函数第一行代码开始执行
    • run:程序开始执行直到命中断点
    • continue:从当前指令向下执行直到下一个断点
  • layout split 很好看啊。今天才知道。

    • focus src / asm / cmd 鼠标聚焦在哪个窗口
      1
      2
      3
      4
      5
      6
      (gdb) focus src
      Focus set to src window.
      (gdb) focus asm
      Focus set to asm window.
      (gdb) focus cmd
      Focus set to cmd window.
  • back trace : bt

    • 查看当前调用堆栈
  • print + 变量名/reg名/值 m : p m

    • 查看变量值
  • x + 变量名m/reg名/值 : x m

    • 查看以m的值作为地址处的内存所存储的值
  • watch + 变量名x : 监控变量x

    • 向下运行 直到该值改变时会停住
  • info inferiors : 查看当前process

    1
    2
    3
    (gdb) info inferiors
    Num Description Executable
    * 1 process 6542 /home/shc/Code/try/lock/pmap
  • info threads : 查看所有 thread

  • thread n : 切换线程

  • si , ni , s , n

    • n/s都是C语言级的断点定位。
      • s会进入C函数内部,但是不会进入没有定位信息的函数(比如没有加-g编译的代码,因为其没有C代码的行数标记,没办法定位)。
      • n不会进入C函数内部。
    • ni/si都是汇编级别的断点定位。
      • si会进入函数内部的第一条汇编一句(C语言的函数 和 通过汇编跳转的函数都会进入)
      • ni不会进入汇编跳转的函数内部。
    • 归纳:
      • 当要进入没有调试信息的库函数调试的时候,用si是我已知的唯一的方法。
      • 如果要进入汇编跳转到的函数比如callq addr,那么si是我已知的唯一的方法
      • 当进入有调试信息的函数,用si和s都可以,但是他们不同,si是定位到汇编级别的第一个语句,但是s是进入到C级别的第一个语句
  • x/20i $pc : 显示当前PC后面的20条指令
  • disas $pc, $pc+20 : 显示接下来20byte的汇编指令
  • display/10i $pc : gdb持续自动显示从pc起的10条指令
  • 命令行工具:
    • pmap + pid
      • 查看一个process的所有地址空间
    • cat /proc/pid/maps
      • 查看一个process的所有地址空间。比pmap更详细。

调试core dump文件

  • 专业的描述是segmentfault,通俗的讲就是程序挂掉了

  • ulimit -c:查看允许生成的core文件大小

  • ulimit -c unlimited:打开core文件开关

  • 开始执行程序,程序挂掉之后发现二进制目录下面有core文件

  • gdb executableFile core文件 :gdb调试core文件,快速发现程序出错代码

    • 会直接显示出程序在哪里挂掉
    • core文件是死去程序的遗言。调试core文件并不是调试我们编写的可执行文件,而是遗言,只读不写。
  • 所以遇到挂掉的程序,根本不用加日志,直接gdb调试core文件,就可以知道在哪里挂掉

  • 知道在那里挂掉之后,就可以直接gdb调试可执行文件,设置断点。

  • 起因:在看Muduo如何获取线程id时,发现有个__thread

CurrentThread : get the thread id of current Thread

  • 获取线程系统调用:gettid(),开销较大,为提高效率,尽量少执行gettid,因此,在第一次得到gettid之后就将tid存起来,下次要用的时候直接返回。
  • currentThread.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //  user通过调用CurrentThread::tid()来获取当前线程的tid
    namespace CurrentThread
    {
    extern thread_local int t_cachedTid; // 每个线程独立的全局变量
    void cacheTid();
    inline int tid()
    {
    if(__glibc_unlikely(t_cachedTid == 0))
    // if(__builtin_expect(t_cachedTid == 0, 0))
    // 告诉编译器 t_cacheTid == 0这件事的值 很有可能为0. 帮助编译器进行分支预测
    {
    cacheTid();
    }
    return t_cachedTid;
    }
    }
  • currentThread.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    namespace CurrentThread
    {
    thread_local int t_cachedTid = 0;
    void cacheTid()
    {
    if(t_cachedTid == 0)
    {
    // 通过Linux系统调用gettid 获取当前线程的tid值
    // 系统调用没提供gettid这个接口 因此需要我们自己通过syscall 进行调用
    // 见Blog csapp-8-异常控制流中的syscall n
    // syscall + 系统调用编号 即可进行系统调用
    t_cachedTid = static_cast<pid_t>(::syscall(SYS_gettid));
    }
    }
    }

builtin_expected

  • __builtin_expect() 是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降。
  • __builtin_expect((x),1)表示 x 的值为真的可能性更大;
  • __builtin_expect((x),0)表示 x 的值为假的可能性更大。
  • likely,unlikely
    1
    2
    # define __glibc_unlikely(cond)	__builtin_expect ((cond), 0)
    # define __glibc_likely(cond) __builtin_expect ((cond), 1)
  • 所以
    • t_cacheTid == 0的可能性很小
      1
      2
      3
      4
      5
      6
      if(__glibc_unlikely(t_cachedTid == 0))
      // if(__builtin_expect(t_cachedTid == 0, 0))
      // 告诉编译器 t_cacheTid == 0这件事的值 很有可能为0. 帮助编译器进行分支预测
      {
      cacheTid(); // 获取Tid
      }

tid

  • top -Hp pid​​​:查看某个进程的线程信息,​​-H​​​ 显示线程信息,​​-p​​指定pid
  • pthread_self返回的不是线程真正的独一无二的那个的tid
  • std::this_thread::get_id() 和 pthread_self相同
  • gettid返回的是线程真正的独一无二的的tid
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int main()
    {
    printf("gettid = %d (syscall real thread id)\n",gettid());
    printf("pthread_self = %ld (POSIX)\n",pthread_self());
    std::cout << "c++11::get_id() = "<<std::this_thread::get_id()<<std::endl;;
    return 0;
    }

    shc@DESKTOP-TVUERHD:~/Code$ ./tid
    gettid = 31721 (syscall real thread id)
    pthread_self = 140053980942144 (POSIX)
    c++11::get_id() = 1

thread_local

作用

  • 存储类说明符以及链接

  • 参考

  • 代码示例

  • 参考

  • thread_local:存储类说明符

    • thread_local 关键词只能搭配在 [命名空间作用域声明的对象]、[在块作用域声明的对象] 及 [静态数据成员]。
    • 它指示对象具有线程存储期
      • 对象的存储在线程开始时分配,而在线程结束时解分配。
      • 每个线程拥有其自身的对象实例。
    • 如果对**块作用域变量(函数内)**只应用了 thread_local 这一个存储类说明符,那么同时也意味着应用了 static。(所以如果在一个函数内声明了thread_local 那么就相当于是这个是一个独属于这个线程的static局部变量)
    • 它能与 static 或 extern 结合,以分别指定内部或外部链接(但静态数据成员始终拥有外部链接)。
    • 一言以蔽之:属于线程的变量。线程间独立,每个线程在自己的栈上有自己的副本。
    • 应当在声明时初始化,该初始化语句在本 thread 中只会执行一次
  • 比__thread

    • __thread:是GCC的关键字,非Unix编程或C语言标准,属于编译器自己实现。__thread只能修饰基础数据类型或者POD类型。
      • 所谓POD就是C语言中传统的struct类型。即无拷贝、析构函数的结构体。
    • thread_local:可以修饰函数内局部对象(自动static);除标准类型外,还可以修饰C++的对象如vector;还可以修饰类中的成员变量,但只能是static。
      • thread_local修饰类内成员变量:static thread_local的变量也需要在类外进行初始化,并且带着thread_local关键字 thread_local int A::count = 0;

code

  • 如下,一个thread_local的全局变量

    • 属于线程的全局变量。全局可见,但每个线程有自己的副本。
      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
      #include <iostream>
      #include <thread>
      #include <mutex>

      std::mutex cout_mutex; // for multiple print

      thread_local int x = 0;

      void thread_func(const std::string& thread_name) {
      for (int i = 0; i < 3; ++i) {
      std::lock_guard<std::mutex> lock(cout_mutex);
      std::cout << "thread[" << thread_name << "]: x = " << ++x << std::endl;
      }
      return;
      }

      int main() {
      std::thread t1(thread_func, "t1");
      std::thread t2(thread_func, "t2");
      t1.join();
      t2.join();
      return 0;
      }

      thread[t1]: x = 1
      thread[t1]: x = 2
      thread[t1]: x = 3
      thread[t2]: x = 1
      thread[t2]: x = 2
      thread[t2]: x = 3
  • 一个thread_local的局部变量

    • 只用thread_local的话相当于static thread_local
      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
      #include <thread>
      #include <mutex>
      std::mutex cout_mutex; //方便多线程打印

      void thread_func(const std::string& thread_name) {
      for (int i = 0; i < 3; ++i) {
      thread_local int x = 0;
      std::lock_guard<std::mutex> lock(cout_mutex);
      std::cout << "thread[" << thread_name << "]: x = " << ++x << std::endl;
      }
      return;
      }

      int main() {
      std::thread t1(thread_func, "t1");
      std::thread t2(thread_func, "t2");
      t1.join();
      t2.join();
      return 0;
      }

      thread[t1]: x = 1
      thread[t1]: x = 2
      thread[t1]: x = 3
      thread[t2]: x = 1
      thread[t2]: x = 2
      thread[t2]: x = 3
  • thread_local 修饰类成员变量

    • 必须是static
  • thread_local 修饰的对象只在其作用域内可见

extern

  • header.h : extern xx
    • 存在一个这样的变量xx。他在某一个.c文件中被定义。任何一个include该header.h 都可以看到并使用这个变量xx。
    • 被多个文件include,但只被某个.c定义了一次。
    • 在.c文件编译完成,进入链接阶段,linker会将所有xx的引用 和 一个 xx的定义关联起来。(xx的定义到已经compiled source files中去找)

  • 死锁
    • 理论以及学校考试上
      • 四个条件:互斥 不可剥夺 请求与保持 循环等待
      • 措施:预防,避免,检测/恢复,忽略
      • 死锁避免中的 银行家算法实现
    • 实际
      • AA-Deadlock
        • Thread自己等待自己释放锁 : funcA持有lock , funcA调用funcB , funcB也需要lock
      • AB-BA-Deadlock
        • T1先拿A后等B,T2先拿B后等A
        • 应当严格按照固定的顺序获得所有锁
阅读全文 »

emplace_back

作用

  • 将原本用来构造临时量的参数传入emplace_back,然后直接在vector容器的内存上调用构造函数;而非在外面构造完临时对象再传入容器,再通过construct在vector上调用拷贝构造。
  • 节省了一个临时对象的构造和析构。
    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

    int main()
    {
    vector<Test> v(10);
    Test t(5);
    v.push_back(t);
    v.emplace_back(t);
    cout<<"======================"<<endl;
    v.push_back(Test(5)); // 临时对象Test(int) -> 右值拷贝构造Test(Test&&) -> 临时对象析构~Test()
    v.emplace_back(Test(5)); // 临时对象Test(int) -> 右值拷贝构造Test(Test&&) -> 临时对象析构~Test()
    cout<<"======================"<<endl;
    // v.push_back(10,20); error
    // 原地构造 无需临时对象
    // emplace 原地构造:将原本用来构造临时量的对象传入vector容器,在vector容器内存上利用这些参数构造对象;而在外面非构造完临时对象再传入容器,再在容器上调用拷贝构造。节省了一个对象的构造和析构
    v.emplace_back(5); // 直接在vector管理的内存上构造 Test(int) 无需临时对象。省去了构造和析构
    v.emplace_back(10,5); // 直接在vector管理的内存上构造 Test(int int) 无需临时对象。省去了构造和析构
    cout<<"======================"<<endl;
    return 0;
    }

    Test(int)
    Test(const Test&)
    Test(const Test&)
    ======================
    Test(int)
    Test(Test&&)
    ~Test()
    Test(int)
    Test(Test&&)
    ~Test()
    ======================
    Test(int)
    Test(int,int)
    ======================
    ~Test()
    ~Test()
    ~Test()
    ~Test()
    ~Test()
    ~Test()
    ~Test()

代码实现

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
//
// Created by dell on 2022-04-13.
//

#include<iostream>

using namespace std;

#include <iostream>
class Test
{
public:
Test(int x) {cout<<"Test(int)"<<endl;}
Test(int a,int b){cout<<"Test(int,int)"<<endl;}
Test(const Test& ){cout<<"Test(const Test&)"<<endl;}
Test(Test &&){cout<<"Test(Test&&)"<<endl;}
~Test(){cout<<"~Test()"<<endl;}
};


/*
* 容器的空间配置器 allocator
* 内存的开辟释放,对象的构造析构,都是通过空间配置器完成.而不是直接通过new delete
* template<class _Ty
* class _Alloc = allocator<_Ty>>
* class vector
*/

// 定义容器的空间配置器
template<typename T>
struct Allocator
{
T* allocate(int size) // 负责内存开辟
{
return (T*)malloc(sizeof(T) * size);
}

T* deallocate(void* p) // 负责内存释放
{
free(p);
return (T*)p;
}

template<typename ...Ty> // 不定个数参数
void construct(T *p,Ty&&... val) // 引用折叠
{
new (p) T(std::forward<Ty>(val)...); // ...不定参数+完美转发 传入相应构造函数
}
/*
template<typename Ty>
void construct(T* p, Ty&& val)
{
new (p) T(std::forward<Ty>(val));
}
*/
/*
void construct(T* p, const T& val) // 负责对象构造(在已有的内存上)
{
new (p) T(val); // 定位new 在p指向的内存上构造(拷贝构造)
}
void construct(T* p, T&& rval) // 参数val匹配右值
{
// 在p处定位new 调用T的拷贝构造函数。
// rval这个右值引用变量本身为左值;但是他所引用的那个变量val是个右值。因此我们要通过mover(val)来实现类型的转换
// move实现的是语法层面的转换,告诉编译器rval是个右值,让他匹配右值拷贝构造函数。并没有对rval本身内存做出什么更改的事情
// 只是改变了语法层面的类型,为了让编译器识别出他(所引用的)是个右值
// 因为val为右值,因此调用拷贝构造函数时,匹配的就是T(&&)
new (p) T(std::move(rval));
}
*/

void destroy(T* p) // 负责对象析构
{
p->~T(); // ~T()
}

};

// 容器底层内存开辟,内存释放,对象构造,对象析构,都通过allocator空间配置器来实现
// 类型Alloc 的类型默认是 空间配置器Allocator<T> 模板名称 + 类型参数 = 类名称
template<typename T, typename Alloc = Allocator<T>>
class vector
{
public: // 默认形参 构造函数:Allocator<T>()
vector<T, Alloc>(int sz = 10, const Alloc& alloc = Allocator<T>())
:_allocator(alloc)
{
// 需要把内存开辟和对象构造分开处理 不然在创建vector的时候,不但开辟内存,还构造了所有元素.正常逻辑应当是我们加入元素.
// _first = new T[sz];
_first = _allocator.allocate(sz); // 开辟内存
_last = _first;
_end = _first + sz;
}

~vector() {
// delete [] _first;
// 析构有效对象
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p); // 析构p对象
}
// ,并释放所有空间
_allocator.deallocate(_first);
_first = _last = _end = nullptr;
}

vector(const vector<T, Alloc>& rhs) // 拷贝构造
{
int sz = rhs._end - rhs._first; // 容器大小
// _first = new T[sz];
_first = _allocator.allocate(sz);
_end = _first + sz;
int len = rhs._last - rhs._first; // 有效元素长度
for (int i = 0; i < len; ++i)
{
_allocator.construct(_first[i], rhs.first[i]);
}
_last = _first + len;
}

vector<T, Alloc>& operator=(const vector<T, Alloc>& rhs) // 拷贝赋值
{
if (this == &rhs) // 与拷贝构造区别:避免自赋值
return *this;

// delete [] _first; // 析构所有对象 销毁原内存
// 析构有效对象 释放所有内存
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);
}
_allocator.deallocate(_first);

int sz = rhs._end - rhs._first;
// _first = new T[sz];
_first = _allocator.allocate(sz); // 分配内存
int len = rhs._last - rhs._first;
for (int i = 0; i < len; ++i)
{
_allocator.construct(_first + i, rhs._first[i]); // 构造有效对象
}
_last = _first + len;
_end = _first + sz;
return *this;
}

// emplace_back实现 引用折叠 + forward万能转发 + 不定模板参数
// emplace实现原地构造,避免临时量的产生:其实就是把原本要构造临时量对象的参数直接传进来,直接在vector容器的内存上定位new,
// 即调用相应构造函数,而不是先构造临时量对象、传入临时量对象,再在vector容器的内存上调用拷贝构造。从而避免一个临时对象的拷贝和析构
template<typename ...Ty> // 不定模板参数 ...代表有很多模板参数
void emplace_back(Ty&&... args)
{
this->_allocator.construct(_last++,std::forward<Ty>(args)...); // ...代表不定个数的参数
}

// 采用引用折叠 + forward完美转发
// 引用折叠:推导出val到底是右值引用还是左值引用。使得函数可以同时接收左值引用和右值引用。
// forward:传参时保留val他(所引用的)到底是一个左值还是右值的信息,即保留引用的类型。
// 避免左值引用和右值引用变量本身都是左值而失去左右值信息,那样传参就只能传给左值。
template<typename Ty>
void push_back(Ty&& val) // CMyString&& + && -》 CMyString&& ; CMyString& + && -> CMyString&
{
if (full())
expand();
_allocator.construct(_last++, std::forward<Ty>(val));
}
// push_back需要写两个 但是核心只有参数列表不同,右值还需要通过move来表示。不同也只是为了向下调用时调用到与左/友值匹配的函数。
/*
void push_back(const T& val)
{
if (full())
expand();
// *_last++ = val; // 元素的operator=
_allocator.construct(_last++, val);
}

// push_back(&&) -> construct(&&) -> new T(&&)(也就是调用 类型的右值拷贝构造,如CMyString(&&))
// 传入的实参为右值时(如CMyString("aa");会匹配到这里)
// push_back了之后,容器里装的对象就和push_back左值一样。后续对于容器的操作也一样.
void push_back(T&& val) // push_back右值
{
if (full())
expand();
// 因为右值变量本身是左值,因此要move一下,让编译器识别他是右值。
_allocator.construct(_last++, std::move(val));
}
*/
// 需要析构对象,并且要把析构对象和释放内存的操作分离开
void pop_back()
{
if (empty())
return;
verify(_last - 1, _last); // 检查删除元素后,有哪些迭代器失效 _last-1:最后一个元素。有对应迭代器 _last:后继位置,有对应迭代器
// --_last; // 只移动指针,并没有析构元素,也没有释放元素管理的内存。
_allocator.destroy(--_last); // 不仅移动指针 还需要析构元素s
}

T back() const
{
if (empty())
throw "vector is empty";
return *(_last - 1);
}

bool empty() const { return _first == _last; }
bool full() const { return _last == _end; }
int size() const { return _last - _first; }
T& operator[](int index) {
if (index < 0 || index >= size())
throw "OutOfRange";
return _first[index];
}
const T& operator[](int index) const {
if (index < 0 || index >= size())
throw "OutOfRange";
return _first[index];
}


// 实现迭代器
// 迭代器一般实现成容器的嵌套类型
class iterator // 迭代器就是包装了遍历方法(++)的一个指针
{
public:
friend class vector<T, Alloc>;
// 产生迭代器对象 自动将自己加入迭代器链表
iterator(vector<T, Alloc>* pvec, T* p = nullptr)
:_pVec(pvec), _ptr(p)
{
// cout << cnt++ << endl;
// 链表头插法 节点内容 节点指向
Iterator_Base* ib = new Iterator_Base(this, _pVec->_head._next);
_pVec->_head._next = ib; // 指针ib是局部变量,会被销毁无所谓。因为ib指向的内存是new出来的啊哈哈哈。不会被销毁的。
}
bool operator!=(const iterator& iter)
{
// _pVec == nullptr 代表迭代器失效
// 首先判断是否是同一容器的迭代器(比较迭代器指向容器的地址是否相同)
if (_pVec == nullptr || _pVec != iter._pVec)
{
throw "iterator incompatable!";
}
return _ptr != iter._ptr;
}
void operator++()
{
// _pVec == nullptr 迭代器失效
if (_pVec == nullptr)
{
throw "iterator inValid";
}
_ptr++;
}
T& operator*()
{
// 检查迭代器失效
if (_pVec == nullptr)
{
throw "iterator inValid";
}
return *_ptr;
}
const T& operator*() const { return *_ptr; }
~iterator()
{
cout << "hh" << endl;
}
private:
// 指向元素
T* _ptr;
// 指明是哪个容器的迭代器 T和Alloc都是已知的(外层vector就确定了) 因为同一迭代器的比较才有效
vector<T, Alloc>* _pVec; // 迭代器是否有效标志
};

// 容器需要提供begin end方法
iterator begin() { return iterator(this, _first); }
iterator end() { return iterator(this, _last); }

iterator insert(iterator it, const T& val)
{
verify(it._ptr - 1, _last); // [it._ptr , last]的迭代器 如果存在,都要失效
T* p = _last; // p是指针 不是迭代器
while (p > it._ptr)
{
_allocator.construct(p, *(p - 1));
_allocator.destroy(p - 1);
--p;
}
_allocator.construct(p, val); // 插入
++_last;
return iterator(this, p);
}
// 1.置迭代器为无效 + 2.移动元素
iterator erase(iterator iter)
{
verify(iter._ptr - 1, _last); // 1.
T* p = iter._ptr; // 2.
while (p < _last - 1)
{
_allocator.destroy(p);
_allocator.construct(p, *(p + 1));
++p;
}
_allocator.destroy(p);
_last--;
return iterator(this, iter._ptr);
}


private:
T* _first; // 指向数组起始位置
T* _last; // 指向数组中有效元素的后继位置
T* _end; // 指向数组空间的后继位置
Alloc _allocator; // 空间配置器对象

// 为应对迭代器失效 增加代码

// 用一个链表来维护每个迭代器之间的顺序(可以当成是一个用来维护(装载)迭代器的容器
// 迭代器节点 _cur指向迭代器 _next指向下一个iterator_base节点
struct Iterator_Base
{
Iterator_Base(iterator* c = nullptr, Iterator_Base* ne = nullptr)
:_cur(c), _next(ne) {}
iterator* _cur;
Iterator_Base* _next;
};
Iterator_Base _head; // 一个容器只有一个迭代器头节点

// 检查
// 将(first,last]之间的元素,如果存在对应的迭代器,那么都要置为失效。并将维护他们的节点从链表中移除。
// 遍历链表
void verify(T* first, T* last)
{
Iterator_Base* pre = &this->_head; // 头节点
Iterator_Base* it = this->_head._next; // 第一个有效节点
while (it != nullptr)
{ // it 节点 _cur 迭代器 _ptr 指向的元素
if (it->_cur->_ptr > first && it->_cur->_ptr <= last)
{
// 标记迭代器失效 :iterator持有的_pVec = nullptr
it->_cur->_pVec = nullptr;
pre->_next = it->_next;
delete it; // delete掉用于维护迭代器的节点
it = pre->_next;
}
else
{
pre = it;
it = it->_next;
}
}
}

// 容器的二倍扩容操作
void expand()
{
int sz = _end - _first;
int len = _last - _first;
// T *p_tmp = new T[sz*2];
// 开辟内存 构造对象
T* p_tmp = _allocator.allocate(sz * 2);
for (int i = 0; i < len; ++i)
{
// p_tmp[i] = _first[i];
_allocator.construct(p_tmp + i, _first[i]);
}

// 析构对象 释放内存
for (T* p = _first; p != _last; ++p)
{
_allocator.destroy(p);
}
// delete [] _first; // 释放原先内存 析构所有对象
_allocator.deallocate(_first);

_first = p_tmp;
_last = _first + len;
_end = _first + 2 * sz;
return;
}
};

线程是CPU基本调度单位(如果os支持线程的话).
CPU调度没有对错,只有好坏,即作出的选择是否合适 .
cpu调度策略 : 先来先服务,最短作业优先,最短剩余时间优先调度,最短剩余时间优先调度,轮转调度,多级队列调度,最高优先级调度,多级反馈队列调度 .

阅读全文 »

阻塞、非阻塞、同步、异步

网络IO阶段一:数据准备

  • 数据准备:根据系统IO操作的就绪状态。
    • 阻塞 blocking
    • 非阻塞 non-blocking
  • 解释
    • 比如API recv
    • 远端是否有数据过来,也即内核对应的sockfd的TCP缓冲区是否有数据可读
    • 阻塞时,如果远端没有数据过来,那么recv会阻塞在那里。
    • 非阻塞时,无论有没有数据过来,recv都会立刻返回。通过recv的返回值,判断状态。
      • size == -1 && errno = EAGAIN 非阻塞没数据
      • size = 0 网络对端关闭
      • size > 0 读到的bytes数

网络IO阶段二:数据读取

  • 数据读写:根据应用程序和内核的交互方式

    • 同步 synchronous
    • 异步 asynchronous
  • 解释

  • 同步 synchronous

    • API recv
    • arg传sockfd,buf
    • (无论阻塞和非阻塞时),如果数据就绪了,sockfdkernel的TCP缓冲区中有数据,那么会将os的内核缓冲区拷贝到应用程序的buf缓冲区,这段时间花的是应用程序自己的时间。等待直到拷贝完成后,recv才返回,应用程序才能向后走。
  • 异步 asynchronous

    • 传sockfd,buf,通知方式
    • os负责监听,tcp缓冲区是否有数据可读,有的话,则内核替我将数据从内核缓冲区拷贝到我传给他的buf。拷贝完后再通过约定好的通知方式通知我。这段拷贝的时间是其他进程花费的时间。不是本进程花费的时间
  • 也即同步需要自己监听 事件什么时候完成(数据从内核缓冲区被拷贝到用户缓冲区)

  • 而异步不需要,当事情完成后,会有别人通知(数据的监听,数据的搬运(读写)都不需要应用程序来花费时间,都是内核做的)

  • 业务的同步异步
    • 与IO的同步异步原理相同
    • 是业务层面的处理逻辑
    • 同步:A发起请求,等待B操作做完事情,得到返回值,继续处理。
    • 异步:A操作告诉B操作它感性确定时间以及通知方式,A操作继续执行自己的业务逻辑了;等B监听到相应的事件发生后,B会通知A,A开始相应的数据处理逻辑。
  • 总结
    • 同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);
    • 异步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果
  • 同步阻塞:int sz = recv(fd,buf,SIZE,0);

  • 同步非阻塞:int sz = recv(fd,buf,SIZE,非阻塞);

  • 异步阻塞:理论上有,但完全就是浪费时间,没必要,没人用。

  • 异步非阻塞:异步一般都是非阻塞。

  • 描述逻辑

    • 用一个IOAPi来讲,比如read/recv
    • 在阻塞/非阻塞下的表现形式
    • 接下来再介绍同步、异步(特殊API aio_read)

陈硕大神原话:在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步
IO。

Linux五种IO模型

Blocking IO

  • 同步阻塞
  • 一直阻塞 效率不高。
  • 分为两阶段
    • 等待数据就绪
    • 拷贝数据从kernel buf -> usr buf

non-Blocking IO

  • 同步非阻塞
  • 我的问题:
    • 真的,就在这里不断轮询有什么用?浪费时间还占用cpu。不如把cpu让给别的线程,在这非阻塞轮询还不如阻塞呢。那非阻塞有什么用?
  • 解决:
    • non-blocking一般是和IO复用配合使用

IO复用(IO multiplexing)

  • epoll/select/poll:同步。异步最大的特点是协商了一个通知事件机制,这里明显没有,就是在等待。

  • read同步

  • IO复用比起轮询和阻塞好在哪?

    • 一次返回多个事件,从而不用多次在内核态和用户态之间切换。

Signal-Driven 信号驱动

  • 在第一个阶段异步,在第二个阶段同步。
  • 如果阻塞的话,需要一直阻塞
  • 如果非阻塞的话,需要一直轮询检查
  • 而信号通知机制,使得我们不必阻塞也不必轮询检查,可以去做应用程序自己的事情,同时减少了系统API的调用次数,提高效率。
  • 很少用。因为信号异步 不好处理

Asychronous 异步

1
2
3
4
5
6
7
8
9
struct aiocb {
int aio_fildes
off_t aio_offset
volatile void *aio_buf
size_t aio_nbytes
int aio_reqprio
struct sigevent aio_sigevent
int aio_lio_opcode
}
  • 异步非阻塞,根本不消耗应用程序的时间,应用程序完全可以去做其他事情,等待内核通知即可
  • 编写困难。

one loop per thread

  • one loop per thread is usually a good model ——libv作者
    • 如果采用one loop per thread的模型,多线程服务器端编程就简化为如何设计一个高效,且易于使用的event loop,然后每个thread run 一个 event loop就行了。 再加上同步、互斥等。
  • event loop 是 non-blocking 网络编程的核心

non-blocking & IO multiplexing

  • 在现实生活中,non-blocking 几乎总是和 IO multiplexing 一起使用,原因有两点:
    • 单单non-blocking:不断轮询busy looping检查某个non-blocking IO操作是否完成,耗费时间,浪费CPU。没人这么做的
    • 单单IO-multiplex:IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO 中 read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket上的 IO 事件了。
      • 例如:epoll返回一个可读事件,用户程序进行读取,会一直循环直到读完这个可读fd,如果我们用的是阻塞的socket的话,那么在read读完之后就会阻塞在这个read上,本线程在这里阻塞住,程序无法继续向下走,也无法返回到epoll中。
  • 所以,当我们提到 non-blocking 的时候,实际上指的是non-blocking + IO-multiplexing,单用其中任何一个都没有办法很好的实现功能。

只记录API关键信息方便快速回忆。API的具体信息还请man。以前初学的时候记录的信息太冗杂了。现在看也不方便看,这回力求精简,痛苦的重写一遍概要。

阅读全文 »