C/C++ Notebook


title: C/C++ Notebook
toc: true

C&C++ Notebook

关键字

thread_local

  • C++中有4种存储周期:
    1. automatic
    2. static
    3. dynamic
    4. thread
  • 有且只有thread_local关键字修饰的变量具有线程周期(thread duration),这些变量(或者说对象)在线程开始的时候被生成(allocated),在线程结束的时候被销毁(deallocated)
  • 每一个线程都拥有一个独立的变量实例.thread_local 可以和staticextern关键字联合使用,这将影响变量的链接属性(to adjust linkage)。
  • 那么,哪些变量可以被声明为thread_local?以下3类都是ok的
    1. 命名空间下的全局变量
    2. 类的static成员变量
    3. 本地变量
  • 既然每个线程都拥有一份独立的thread_local变量,那么就有2个问题需要考虑:
    1. 各线程的thread_local变量是如何初始化的
    2. 各线程的thread_local变量在初始化之后拥有怎样的生命周期,特别是被声明为thread_local的本地变量(local variables)
  • 声明为thread_local的本地变量在线程中是持续存在的,不同于普通临时变量的生命周期,它具有static变量一样的初始化特征和生命周期,虽然它并没有被声明为static。例子中foo函数中的thread_local变量 i 在每个线程第一次执行到的时候初始化,在每个线程各自累加,在线程结束时释放。

标准库

function

  • 解决了函数指针不适用的场景(函数签名,参数)
std::bind
  • (9条消息) C++11中的std::bind_大猪的博客-CSDN博客
  • bind是这样一种机制,**它可以预先把指定可调用实体的某些参数绑定到已有的变量,**产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。
  • bind的思想实际上是一种延迟计算的思想,将可调用对象保存起来,然后在需要的时候再调用。而且这种绑定是非常灵活的,不论是普通函数、函数对象、还是成员函数都可以绑定,而且其参数可以支持占位符,比如你可以这样绑定一个二元函数auto f = bind(&func, _1, _2);,调用的时候通过f(1,2)实现调用。

memory

智能指针
  • 程序内存空间
    • 静态内存
    • 栈内存
    • 内存池(堆)
  • 动态内存管理(new,delete)
    • 两种问题
      • 忘记释放内存,会造成内存泄漏;
      • 尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针;
  • 引入智能指针
    • 行为类似常规指针,区别在于它负责自动释放所指向的对象
    • 三种智能指针
      • shared_ptr:允许多个指针指向同一个对象,
      • unique_ptr:“独占”所指向的对象
      • weak_ptr:弱引用,指向shared_ptr所管理对象
share_ptr
  • shared_ptr<string> p1;
    shared_ptr<list<int>> p2;
    
  • 默认初始化的智能指针中保存着一个空指针

  • 基本操作

    • shared_ptr和unique_ptr都支持的操作:

      这里写图片描述

    • 如下表所示是shared_ptr特有的操作: 这里写图片描述

  • make_shared函数

    • 最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数

    • 使用:

      shared_ptr<string> p4 = make_shared<string>(10,'9');
      
  • 拷贝构造

    auto p = make_shared<int>(42);
    auto q(p);
    
    • 当进行拷贝和赋值时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。

    • 每个shared_ptr都有一个关联的计数器,通常称其为引用计数

    • 一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

      auto r = make_shared<int>(42);//r指向的int只有一个引用者
      r=q;//给r赋值,令它指向另一个地址
          //递增q指向的对象的引用计数
          //递减r原来指向的对象的引用计数
          //r原来指向的对象已没有引用者,会自动释放
      
  • shared_ptr自动销毁所管理的对象

    • 当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象,它是通过另一个特殊的成员函数-析构函数完成销毁工作的,类似于构造函数,每个类都有一个析构函数。析构函数控制对象销毁时做什么操作。析构函数一般用来释放对象所分配的资源。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它所占用的内存。(面试)
  • shared_ptr还会自动释放相关联的内存

    • 当动态对象不再被使用时,shared_ptr类还会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
  • 使用了动态生存期的资源的类

    • 程序使用动态内存的原因:
      • 程序不知道自己需要使用多少对象
      • 程序不知道所需对象的准确类型
      • 程序需要在多个对象间共享数据???
  • 动态分配的const对象

    const int *pci = new const int(1024);
    //分配并初始化一个const int
    const string *pcs = new const string;
    //分配并默认初始化一个const的空string
    
    • 类似其他任何const对象,一个动态分配的const对象必须进行初始化
    • 对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象就必须显式初始化。由于分配的对象是const的,new返回的指针就是一个指向const的指针。
  • 内存耗尽: 虽然现代计算机通常都配备大容量内村,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,new表达式就会失败。默认情况下,如果new不能分配所需的内存空间,他会抛出一个bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常

    //如果分配失败,new返回一个空指针
    int *p1 = new int;//如果分配失败,new抛出std::bad_alloc
    int *p2 = new (nothrow)int;//如果分配失败,new返回一个空指针
    
    • 我们称这种形式的new为定位new,定位new表达式允许我们向new传递额外的参数,在例子中我们传给它一个由标准库定义的nothrow的对象,如果将nothrow传递给new,我们的意图是告诉它不要抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都在头文件new中。
  • 释放动态内存

    • 为了防止内存耗尽,在动态内存使用完之后,必须将其归还给系统,使用delete归还。
  • 指针值和delete

    • 我们传递给delete的指针必须指向动态内存,或者是一个空指针。释放一块并非new分配的内存或者将相同的指针释放多次,其行为是未定义的。即使delete后面跟的是指向静态分配的对象或者已经释放的空间,编译还是能够通过,实际上是错误的。
  • 动态对象的生存周期直到被释放时为止 由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放,但是通过内置指针类型来管理的内存就不是这样了,内置类型指针管理的动态对象,直到被显式释放之前都是存在的,所以调用这必须记得释放内存。

  • 使用new和delete管理动态内存常出现的问题: (1)忘记delete内存 (2)使用已经释放的对象 (3)同一块内存释放两次

  • delete之后重置指针值

    • 在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址
    • 有一种方法可以避免悬空指针的问题:在指针即将要离开其作用于之前释放掉它所关联的内存
    • 如果我们需要保留指针可以在delete之后将nullptr赋予指针,这样就清楚的指出指针不指向任何对象。
    • 动态内存的一个基本问题是可能多个指针指向相同的内存
  • shared_ptr和new结合使用

    • 如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针

    • shared_ptr<int> p1 = new int(1024);//错误:必须使用直接初始化形式
      shared_ptr<int> p2(new int(1024));//正确:使用了直接初始化形式
      
  • 下表为定义和改变shared_ptr的其他方法: 这里写图片描述

  • 不要混合使用普通指针和智能指针

    • 如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。

    • shared_ptr<int> p(new int(42));//引用计数为1
      int *q = p.get();//正确:但使用q时要注意,不要让它管理的指针被释放
      {
          //新程序块
          //未定义:两个独立的share_ptr指向相同的内存
          shared_ptr(q);
          
      }//程序块结束,q被销毁,它指向的内存被释放
      int foo = *p;//未定义,p指向的内存已经被释放了
      

      p和q指向相同的一块内部才能,由于是相互独立创建,因此各自的引用计数都是1,当q所在的程序块结束时,q被销毁,这会导致q指向的内存被释放,p这时候就变成一个空悬指针,再次使用时,将发生未定义的行为,当p被销毁时,这块空间会被二次delete

  • 智能指针和异常 如果使用智能指针,即使程序块过早结束,智能指针也能确保在内存不再需要时将其释放,sp是一个shared_ptr,因此sp销毁时会检测引用计数,当发生异常时,我们直接管理的内存是不会自动释放的。如果使用内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放。

  • 使用我们自己的释放操作 默认情况下,shared_ptr假定他们指向的是动态内存,因此当一个shared_ptr被销毁时,会自动执行delete操作,为了用shared_ptr来管理一个connection,我们必须首先必须定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针进行释放的操作。

  • 智能指针陷阱: (1)不使用相同的内置指针值初始化(或reset)多个智能指针。 (2)不delete get()返回的指针 (3)不使用get()初始化或reset另一个智能指针 (4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了 (5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

unique_ptr
  • 某个时刻只能有一个unique_ptr指向一个给定对象,由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作

  • 下表是unique的操作: 这里写图片描述

  • 虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique

    //将所有权从p1(指向string Stegosaurus)转移给p2
    unique_ptr<string> p2(p1.release());//release将p1置为空
    unique_ptr<string>p3(new string("Trex"));
    //将所有权从p3转移到p2
    p2.reset(p3.release());//reset释放了p2原来指向的内存
    
  • 不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.

    unique_ptr<int> clone(int p)
    {
    	//正确:从int*创建一个unique_ptr<int>
    	return unique_ptr<int>(new int(p));
    }
    
  • 还可以返回一个局部对象的拷贝:

    unique_ptr<int> clone(int p)
    {
    	unique_ptr<int> ret(new int(p));
    	return ret;
    }
    
  • 向后兼容:auto_ptr 标准库的较早版本包含了一个名为auto_ptr的类,它具有uniqued_ptr的部分特性,但不是全部。

  • 用unique_ptr传递删除器 unique_ptr默认使用delete释放它指向的对象,我们可以重载一个unique_ptr中默认的删除器 我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象删除器。

weak_ptr
  • weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。 weak_ptr的操作
  • 这里写图片描述
  • 由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针
enable_shared_from_this

常用函数

dynamic_cast

  • 功能:将基类的指针或引用安全地转换为派生类的指针或引用,并用派生类的指针或引用调用非虚函数。如果是基类指针或引用调用的是虚函数无需转换就能在运行时调用派生类的虚函数

  • 用法:

    Inherit* pInherit = dynamic_cast<Inherit*>(pbase);
    

fcntl

STL

set

  • lower_bound
    • 返回第一个大于等于x的数,如果没找到,返回末尾的迭代器位置

常用语法

引用

  • 引用就是变量的别名,操作一个变量的引用也就相当于操作变量本身,这一点跟指针很类似,但是操作引用不用像操作指针一样,利用取地址符号,很不方便。而操作引用的话,则跟操作普通变量一样,所以C++之中更加鼓励使用引用。

  • C++函数为什么要使用引用?

    C语言之中大量利用指针作为形参或者函数返回值,这是由于值拷贝会有很大的消耗(比如传入传出一个大的结构体)。所以在C++之中使用引用作为函数参数和返回值的目的和使用指针是一样的。而且形式上更加直观,所以C++提倡使用引用。

  • C++返回引用?

    • C++之中函数的返回分为以下几种情况;
    • main的返回值:返回0表示运行成功。
      • 返回非引用类型:函数的返回值用于初始化在跳出函数时候创建的临时对象。用函数返回值来初始化临时对象与用实参初始化形参的方法是一样的。如果返回类型不是引用的话,在函数返回的地方,会将返回值复制给临时对象。且其返回值既可以是局部对象,也可以是表达式的结果.
      • 返回引用:当函数返回引用类型的时候,没有复制返回值,而是返回对象的引用(即对象本身)。
      • 函数返回引用:实际上是一个变量的内存地址,既然是内存地址的话,那么肯定可以读写该地址所对应的内存区域的值,即就是“左值”,可以出现在赋值语句的左边。

​ 1)函数返回引用的时候,可以利用全局变量(作为函数返回),或者在函数的形参表中有引用或者指针(作为函数返回),这两者有一个共同点,就是返回执行完毕以后,变量依然存在,那么返回的引用才有意义。

常成员函数和变量

  • 作用: 无法修改数据成员,一般用来修饰Get的函数

  • 本质:this指针类型:const T* const

  • 意义:让编译器提醒开发者该函数不能修改类的成员变量,用于const对象(引用或指针)

  • const 常成员函数

    • 普通的类成员函数this指针类型: T* const(表示该指针本身不能被修改)T为该类

      例如:SetNumber()的this指针类型为:CInterger* const;

    • 常成员函数this指针类型:const T* const

      • 表示该指针本身不能被修改
      • 表示指针指向的内容不能被修改
      • this指针指向对象,所以常成员函数this指向的内容(成员变量)不能被修改
  • 如何非的在常成员函数内修改成员变量?

  • 将类中的成员变量声明为mutable;

    private:
        mutable int m_nNumber;
    
  • const常成员变量

    • 常成员变量在定义的时候要初始化,因为其定义后不允许被修改。

静态成员函数和变量

  • 类静态数据成员在编译时创建并初始化:在该类的任何对象建立之前就存在,不属于任何对象,而非静态类成员变量则是属于对象所有的。类静态数据成员只有一个拷贝,为所有此类的对象所共享。特别需要注意的一点是:静态数据成员不能在类中初始化(对于常量静态类变量有待考证,好像可以在类外或main()函数之前定义,初始化可以放在类中),一般在类外和main()函数之前初始化,缺省时初始化为0。静态数据成员用来定义类的各个对象所公有的数据,比全局变量更安全。
  • 类静态成员函数属于整个类,不属于某个对象,由该类所有对象共享。静态成员可定义为inline函数。一般情况下静态成员函数用于访问同一类中的静态数据成员或全局变量,而不访问非静态成员,如需访问非静态成员,需要将对象作为参数,通过对象名访问该对象的非静态成员。静态成员函数也可以在类外定义,此时不可以用static修饰。静态成员函数存在的原因是什么呢?主要是在建立任何对象之前可用它来访问静态数据成员,普通函数不能实现此功能。

Lambda表达式

  • 定义并创建匿名的函数对象,以简化编程工作。
  • [捕获外部变量列表] (形参列表) mutable(用来说用是否可以修改捕获的变量) 或 exception(异常设定) -> 返回值类型 {函数体}
    • 黑体部分是必须存在的
  • C++ 11 Lambda表达式 - 滴水瓦 - 博客园 (cnblogs.com)

开发常用机制

协程

  • 从头到尾理解有栈协程实现原理 - 知乎 (zhihu.com)

  • 关于协程的概念,在网上没有找到正式的解释,下面就说说个人的理解,(有栈)协程可以理解为一个用户态下的线程,在用户态下进行线程(协程)的上下文切换。但是和传统的线程不同的是:线程是抢占式执行,当发生系统调用或者中断的时候,交由OS调度执行;而协程是通过yield主动让出cpu所有权,切换到其他协程执行

  • 进程、线程

    • 栈:栈用于维护函数调用的上下文,包括函数的参数,局部变量等等
    • 堆:用来容纳程序中动态分配的内存区域,当程序使用malloc和new分配的内存就来自于堆里
    • 可执行文件映像
    • 保留区,是对内存中受到保护而禁止访问的内存区域的总称。
  • 下图是linux下一个进程的典型内存布局: 在经典的操作系统中,栈总是 向下增长 ,从高地址向低地址增长,其中栈顶指针存储在 [E|R]SP 寄存器中,而堆则总是 向上增长 ,从低地址向高地址增长。

    img

  • 栈的增长方式

栈在程序运行中具有举足轻重的地位,最重要的,栈保存了一个函数调用所需要维护的信息,这常常被称为堆栈帧,堆栈帧一般包括下面几方面内容:

  1. 函数的返回地址和参数
  2. 临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  3. 保存的上下文:包括在函数调用前后需要保存不变的寄存器的值

一个函数的堆栈帧用 [E|R]SP,[E|R]BP ,这两个寄存器划定范围,[E|R]SP始终指向栈顶的位置称为栈指针寄存器,[E|R]BP指向堆栈帧的一个固定位置,[E|R]BP 又被称为帧指针,一般函数中的局部变量靠 [E|R]BP 加上偏移量寻找。

[E|R]SP表示esp或者rsp寄存器,esp表示32位x86架构下的栈指针寄存器,rsp表示64位x86架构下的栈指针寄存器,同理于[E|R]BP。 帧指针并不是必须的, x86-64过程中的栈帧通常有固定的大小,在调用过程中栈指针保持固定的位置,使得可以通过相对于栈指针的偏移量来访问数据^1

下面我们就简单讲解一下当程序调用一个简单的函数时,线程中的栈是如何增长的。假设有一个foo函数

int foo(int m, int n){
  int a = 0; // #i
  ....
}

该函数对应的堆栈帧的内存空间如下所示,一个函数的堆栈帧增长过程是这样的:

  1. 把所有或一部分参数加入栈中,如果有其他参数没有入栈,那么使用某些寄存器传递
  2. 把当前指令的下一条指令地址压入栈中
  3. 跳转到函数体执行:
  4. 把[e|r]bp压入栈中,指向上一个函数堆栈帧中的帧指针的位置
  5. 保存调用前后需要保存不变的寄存器的值
  6. 将局部变量压入栈中

当函数调用返回之后,相应的函数堆栈帧也会弹出,弹出的流程不是本篇文章的重点,在此就不详细讲解,感兴趣的推荐看 程序员的自我修养 中讲解堆栈的部分。

img

epoll

(11条消息) Epoll原理解析*~~ LINUX ~~-CSDN博客*epoll

一、概念初探
  • epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现。
  • IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
  • IO多路复用,以后也会有详细讲解。
二、I/O
  • 输入输出(input/output)的对象可以是文件(file), 网络(socket),进程之间的管道(pipe)。在linux系统中,都用文件描述符(fd)来表示。
三、事件
  • 可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件。(可读:内核缓冲区非空,有数据可以读取)
  • 可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件。(可写:内核缓冲区不满,有空闲空间可以写入)
四、通知机制
  • 通知机制,就是当事件发生的时候,则主动通知。通知机制的反面,就是轮询机制。
五、epoll的通俗解释
  • 结合以上三条,epoll的通俗解释是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制

  • epoll的API详解

  • epoll的核心是3个API,核心数据结构是:1个红黑树和1个链表

    • int epoll_create(int size)

      • 功能:内核会产生一个epoll 实例数据结构并返回一个文件描述符,这个特殊的描述符就是epoll实例的句柄,后面的两个接口都以它为中心(即epfd形参)。size参数表示所要监视文件描述符的最大值,不过在后来的Linux版本中已经被弃用(同时,size不要传0,会报invalid argument错误)
    • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

      • 功能:将被监听的描述符添加到红黑树或从红黑树中删除或者对监听事件进行修改

      • 对于需要监视的文件描述符集合,epoll_ctl对红黑树进行管理,红黑树中每个成员由描述符值和所要监控的文件描述符指向的文件表项的引用等组成。

      • op参数说明操作类型:

        EPOLL_CTL_ADD:向interest list添加一个需要监视的描述符

        EPOLL_CTL_DEL:从interest list中删除一个描述符

        EPOLL_CTL_MOD:修改interest list中一个描述符

      • struct epoll_event结构描述一个文件描述符的epoll行为。

        • typedef union epoll_data {
              void *ptr; /* 指向用户自定义数据 */
              int fd; /* 注册的文件描述符 */
              uint32_t u32; /* 32-bit integer */
              uint64_t u64; /* 64-bit integer */
          } epoll_data_t;
          
          struct epoll_event {
              uint32_t events; /* 描述epoll事件 */
              epoll_data_t data; /* 见上面的结构体 */
          };
          
        • 在使用epoll_wait函数返回处于ready状态的描述符列表时,data域是唯一能给出描述符信息的字段,所以在调用epoll_ctl加入一个需要检测的描述符时,一定要在此域写入描述符相关信息

        • events域是bit mask,描述一组epoll事件,在epoll_ctl调用中解释为:描述符所期望的epoll事件,可多选

        • 常用的epoll事件描述如下:

          • EPOLLIN:描述符处于可读状态

            EPOLLOUT:描述符处于可写状态

            EPOLLET(ET模式):将epoll event通知模式设置成edge triggered(PS:事件就绪时,假设对事件没做处理,内核不会反复通知事件就绪,如果此时缓存区没有可读数据,则epoll_wait不会返回EPOLLIN,如果此时缓冲区有可读数据,则epoll_wait会返回一次EPOLLIN)

            EPOLLLT(LT模式,默认):事件就绪时,假设对事件没做处理,内核会反复通知事件就绪如果此时缓存区没有可读数据,则epoll_wait不会返回EPOLLIN,如果此时缓冲区有可读数据,则epoll_wait会持续返回EPOLLIN

            EPOLLONESHOT:第一次进行通知,之后不再监测

            EPOLLHUP:本端描述符产生一个挂断事件,默认监测事件

            EPOLLRDHUP:对端描述符产生一个挂断事件

            EPOLLPRI:由带外数据触发

            EPOLLERR:描述符产生错误时触发,默认检测事件

      • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

        • 功能:阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。
        • events: 用来记录被触发的events,其大小应该和maxevents一致
        • maxevents: 返回的events的最大个数
        • 处于ready状态的那些文件描述符会被复制进ready list中,epoll_wait用于向用户进程返回ready list。events和maxevents两个参数描述一个由用户分配的struct epoll event数组,调用返回时,内核将ready list复制到这个数组中,并将实际复制的个数作为返回值。注意,如果ready list比maxevents长,则只能复制前maxevents个成员;反之,则能够完全复制ready list。
        • 另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件
        • 参数timeout描述在函数调用中阻塞时间上限,单位是ms:
          • timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
          • timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
          • timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。

socket

总概念
  • (13条消息) Socket原理讲解Tony-jiang的博客-CSDN博客socket

  • Socket是应用层与TCP/IP协议族通信的中间软件抽象层,一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定

    img

    • 服务器端
      • 初始化Socket
      • 与端口绑定(bind)
      • 对端口进行监听(listen)
      • 调用accept阻塞,等待客户端连接
    • 客户端
      • 初始化Socket
      • 连接服务器(connect)
      • 连接成功后,可发送数据
  • 利用三元组(ip地址,协议,端口)可以标识网络的进程

基本函数
  • socket()

    int socket(int domain, int type, int protocol);
    
    • 相当于文件的打开操作,创建一个socket描述符
    • 参数:
      • domain:协议族
        • AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等
      • type:socket类型。
        • 常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
      • protocol:协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等
  • bind()

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • 把一个地址族中的特定地址赋给socket

    • 参数:

      • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket

      • addr:一个const struct sockaddr *指针

        //ipv4
        struct sockaddr_in {
            sa_family_t    sin_family; 
            in_port_t      sin_port;   
            struct in_addr sin_addr;   
        };
        
        
        struct in_addr {
            uint32_t       s_addr;     
        };
        //ipv6
        struct sockaddr_in6 { 
            sa_family_t     sin6_family;    
            in_port_t       sin6_port;      
            uint32_t        sin6_flowinfo;  
            struct in6_addr sin6_addr;      
            uint32_t        sin6_scope_id;  
        };
        
        struct in6_addr { 
            unsigned char   s6_addr[16];    
        };
        
      • addrlen:对应的是地址的长度。

    • 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个

    • 网络字节序与主机字节序

      • 主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
        • Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
        • Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
      • 网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。**由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。**字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
      • htonl()–“Host to Network Long” ntohl()–“Network to Host Long” htons()–“Host to Network Short” ntohs()–“Network to Host Short”
  • listen()、connect()

    int listen(int sockfd, int backlog);
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    • 如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
    • isten函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
  • accept()

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    • TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
    • accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
    • 注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
  • read()、write()等函数

  • close()

    int close(int fd);
    

pipe

报错

c++ primer

第七章、类

LINUX

常用命令

系统

ipcs
  • ipcs命令用于报告共享内存、信号量和消息队列信息。
    • ipcs -a:列出共享内存、信号量和消息队列信息。
    • ipcs -l:列出系统限额。
    • ipcs -u:列出当前使用情况。
ulimit
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值