0%

2020年4月15日 下午12:04

序:
  • 为什么会有这篇文章?
    • 因为我即使在学习、使用linux命令的过程中详细的做了笔记,但是如果半个月不用真的就忘了大半,每次返回来在复习笔记的过程中还是觉得学习路线不清晰,都是散乱的点,所以我就尝试能不能再找一个角度进行总结,所以就有了这篇文章。
  • 那为什么要以对象单元为角度呢?
    • 其实这个角度我自己起的名字,我的初衷是希望可以结合linux系统的知识反过来推理linux应该提供哪些命令帮助程序员了解linux的运行情况。那么首当其冲的就是以进程为单位,因为linux系统中所有的任务都是一个或者多个进行在处理。对于一个进程进行socket文件读写的任务,从理论上来说我们既可以从进程的角度进行描述,也可以从设备读写的角度进行描述,也可以从网络的角度进行描述,正式因为这种角度的多样性,linux需要为不同需要的程序员来提供各自合适的工具。

linux为开发者提供的工具包

curl
traceroute
客户端操作进程的方式:
Linux 守护进程的启动方法:bg、disown、nohup、tmux、sytemd

以硬件设备为单位,查看硬件的执行情况

ifconfig以网卡为单位
iftop网卡为单位
根据iostate判断计算机瓶颈读写设备为单位
查看系统版本和管理员
查询cpu信息cpu

以进程为单位,查看linux各个子系统的执行情况:

htop进程为单位
top进程为单位

以其他为单位,查看子系统的执行情况

netstat以连接为单位
iptable以规则为单位
看懂本机上的路由表,并操作以路由规则为单位
硬盘:
df目录结构
硬盘目录结构

2020年4月13日 下午12:19

在讲解这个过程的同时,也可以一并理解一下几个问题:

  1. 为什么需要文件描述符?
  2. 文件描述符fd与inode有关系吗,有怎样的关系?
  3. linux内核是如何封装各种抽象文件的读写接口的,能做到所谓的“一切皆文件”?
  4. 不同的抽象文件,他们之间的读写操作在内核中有什么区别?也就是linux内核它封装了什么?(等同于上一个问题)
  5. 你怎样验证你说的是正确的?有没有可以证明的方式?

讲解的角度、方法:

我们这里采用自顶向下的角度来讲解linux中是如何完成文件的读写。所谓自顶向下其实就是按着程序员代码不断展开,到达系统调用,在系统调用中就需要看linux内核的实现了,这里我们绕过内核的具体代码实现,直接找到通过系统内核系统调用之后处理的结果什么,通过这样间接的方式来验证我们的思路正确性,具体来说就是看能够在linux系统中找到系统内核调用过程中使用的文件以及对应的文件描述符,这些文件或者文件描述符其实就是系统内核调用在运行过程中留下的脚印👣,我们可以按图索骥看看内核到底干了点什么。

linux中,是如何完成文件的读写

  1. 根据操作系统的我们已有的知识储备,我们知道操作系统是很讲究设计的,也就是各个部门之间的分工合作,并且有上下级之间的封装调用,可以做到下级对上级来说,下级的工作是透明的。具体的在linux中的体现就是:区分用户态和内核态。从linux设计的角度来说,希望让竟可能的降低系统的使用者也就是程序员的使用难度,给他们封装出一些丰富的接口,够他们能够完成功能就可以了,程序员你就别管我操作系统是如何实现你调用的接口了。
  2. 正是由于来于这样一种考虑,linux设计了文件描述符:当一个文件被一个进程打开,就会创建一个文件描述符,这里的文件描述符可以理解成字节流的接口,接口这个词就能很清晰的体现了文件描述符在linux中的本质特性。那么,从用户态这个层面来看,其实并不是一切皆文件,而是一切皆文件描述符。
  3. 那么对于有追求的程序员来说,他不满足现状,他希望可以弄明白linux内核是如何实现文件的读写的。这是我们就需要思考一个问题,难道整个文件系统有两层吗,一层是用户态,一层是内核态,内核态就能直接操作硬盘上的文件吗?其实,我们单独拿出内核态来看,它为了实现文件的读写,其实也有所谓的层次结构,这中间也进行了精妙的设计。这里面最关键的一个设计就是inode,inode将我们操作的文件进行抽象,成为我们操作文件读写时最小的操作单元,也就是说,在内核态中其实他操作是的inode,可千万别以为我们直接操作的是赤裸裸的文件,inode就可以很要的封装了各个文件之间大小类型之间的区别,可以说inode是实现文件读写最最核心的关键,其他的类似于管理不同的抽象文件读写,其实都是在inode基础上的业务逻辑,业务逻辑可以频繁的更换,但是基础的inode这层定义逻辑是一定不能变的。这里我们就知道inode真的是特别特别重要,理解inode对理解操作系统也是一个关键。
  4. 认识到inode的重要性,我们接下来说说linux是如何考虑在inode的基础上完成所谓的业务逻辑,来实现不同的抽象文件的读写操作。
    1. inode的重要性就像是数据结构与算法中数据结构对算法的作用,inode就是这里的数据结构,在做算法题的时候,我们知道如果我们设计出了这道题需要的数据结构,那么写算法在熟练语法的基础上就是分分钟的事情
    2. 在inode这套数据结构之上,我们可以这样认为:从内核态的角度来看,不是所谓的一切皆文件描述符,而是一切皆inode,因为内核态中文件描述符是不存在,在从用户态到内核态时文件描述符已近展开了成为了inode。文件描述符fd和inode其实都是一个非负整数,这点他们是相同的。
    3. 如何证明inode是可以对抽象文件,eg:tcp socket进行抽象表示呢?在linux一切皆文件之tcp socket描述符(三) - wilson排球 - 博客园就可以找到证据。
    4. 要实现不同文件类型的读写,我们考虑到linux是一个各种子系统的集合 将操作系统分解,看看这个庞大的系统中包含哪些子系统,这其中就包括网络子系统,当我们需要完成基于socket、tcp socket的文件读写的时候,就需要依赖于网络子系统,把这部分工作承包给它;还有输入输出系统,当我们需要对块设备进行读写时,我们依然需要依赖于它;还有对字符流设备的读写,这里的例子有ssh连接的连接过程,其实就是一个文件读写的过程,它的本质不变,依然可以拆解为单个的文件读写过程,只不过ssh连接进行操作的过程并不是依靠单独唯一的进程、文件,它需要涉及到进行的fork,以及各个进程都会有自己对应的操作文件组。在学习的过程中,一定要明白这部分是属于业务逻辑,比如在ssh中,可以称作为client和server进行交互的逻辑。在学习的时候,容易沉溺于业务逻辑中,而忘记了文件读写的本质操作。业务逻辑会根据业务的不同随时变化,但是文件读写这样基本的操作是永远不会变的,各种复杂的业务也都是有多个文件读写操作进行组合完成的。
    5. 那么现在回答:单独的文件读写的本质是什么,是如何完成的?
      1. 一个进程A启动,那么并不是进打开一个文件,而是会打开各种各样的文件,这这些文件中有很多都是这个进行它所依赖的系统库文件,在这些依赖的系统库文件之外,才有我们真正进行读写的文件,这些文件在打开之后,会有对应的文件描述符
      2. 我们可以在/proc/A/fd找到这些文件描述符的链接,这个链接就会指向我们真正操作的文件位置。其实这个位置是给内核态使用的,他拿上这个位置上的文件进行操作。在从用户态到内核态的过程中,我们就依赖于这个链接完成用户态到内核态的转换。这个位置可以是linux下文件的真实路径,这部分工作由linux文件子系统完成,另外这个位置也可以直接指向inode的编号,在socket链接中我们就以看到这个inode编号。
      3. 简单来说,读写文件的核心是用户态到内核态的一次转换过程,至于到达内核态之后,会根据你操作的文件类型,内核会选择不同的助手子系统来完成真正的inode读写操作。
      4. 对node的读写,就是读写文件的最后一步。至于要问如何完成对node的读写,我下次在写。

2020年4月12日 下午2:42

还是从需求的角度进行分析,理解control、logic、data、泛型、函数式编程

  1. 老一辈的有经验的程序员认识到:我们需要将代码中的control,logic,data进行分离,这样不管我们看别人的代码,还是自己写代码都会更加的有逻辑,更容易看懂并写出,而不仅仅是完成功能就可以了
  2. 那我们如何实现所谓的control,logic,data的分离呢?
    1. 首先我们需要将data进行分离,因为data与control和logic的区别更大
    2. 这个操作我们把它叫做泛型,当然在泛型的过程中并不是使用一个T就可以了,我们需要将泛型和迭代一起完成,详见 模板编程:分步骤STL如何实现泛型
  3. 实现泛型之后,我们如何将control和logic进行分离呢?
    1. 首先,我们需要明白什么是contorl什么是logic,control我把它理解成是调度好像更好理解一些;而logic其实就是我们业务功能。当我们需要更换业务功能时,是可以使用同一个套control进行调度的。可以把control和logic之间的关系可以称作为“范llogic”,这个名字去自与泛型。一个control的例子:map_reduce_filter 就是control,他们只负责遍历。
    2. 这时候,我们将control和logic进行分离之后进行的编程方法叫做函数式编程。当然,上面说的从control和logic进行分离角度来认识函数式编程,其实函数式编程的起源是来自于数学上的函数,函数有有两个特性:stateless,immutable,而函数式编程也必须满足这两个条件,最终能够做到并行执行和copy-paste。两个角度不同而已。
  4. 此时,我们有了实现control,logic,data的分离的方法,也引出了泛型编程和函数编程,那么泛型编程和函数编程具体在语言中该如何进行实现呢?
    1. 函数式编程的实现:
      1. 函数式编程其实还是程序员逻辑角度的不同,并不需要语言、编译器对其进行支持(当然有些语言在设计语法的时候就考虑到了函数式编程,甚至只能进行函数式编程,那么就可以在我们使用函数式编程的时候提供语法层面的便利)
      2. 函数式编程其实就是逻辑的抽象,比过程式这样扁平的设计更加立体,有更多的上下级函数之间的调用封装。
      3. 明白了函数式的思想,那么我们看看代码中具体如何实现函数式呢:
        1. 利用函数编程的三驾马车:filter_map_reduce,把for循环进行函数的封装,隐藏起来。
        2. 递归代替for循环。
        3. 其实,如果将所有的函数式编程的调用过程展开,最终其实还是会变回for循环,只不过由于上下级的调用封装,我们将for循环进行了隐藏,你看不见,并不代表没有,这部分内容其实可以通过map_reduce的实现来看出,map_reduce其实就是通过for循环来实现的。
      4. 为什么拿掉for是实现函数式编程的关键?拿掉之后对有什么效果?
        1. 一般来说,for循环中for本身是control部分,而是其中执行的操作是logic部分,因此与我们要分离control和logic的初衷所违背,成为重灾区
        2. 处理方法是,将for循环按这两部分进拆分:循环部分让map/reduce这些控制来实现,而logic部分作为control部分的参数进行传入。通过这样就实现了control和logic的分离
    2. 而实现泛型,不仅需要我们程序员逻辑代码上的改变,也需要语言本身、编译器对齐进行支持才可以实现。

[toc]

第一章:

一、基础题

  1. 请说出C++语言的优点,缺点,和主要用途?(涉及知识点:1-4节 C++特点, 1-5 C++作用

    优点:

    • 强大的抽象封装能力:这让C++语言具备了强大的开发工程能力,在封装的同时C++最大程度的保留了高性能;
    • 高性能:运行快,快并且占用资源少一直是C++语言的追求;
    • 低功耗:特别适合在各种微型的嵌入式设备中运行高效的程序;

    缺点:

    • 语法相对复杂,细节比较多,学习曲线比较陡;
    • 需要一些好的规范和范式,否则代码很难维护;

二、提高题

  1. 请参考课程演示代码”CPPDemo1”中的C++面向对象方式,思考C面向过程方式中如何实现trace功能在开关打开状态下写入到文件中,并想想这两种方式各自的优缺点?(涉及知识点:1-3节 C++vsC,面向对象vs面向过程
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
#include <stdio.h>
static bool trigger;
void turnOn()
{
trigger = true;
}
void turnOff()
{
trigger = false;
}
void write(char *s,int n)
{
FILE *fp = fopen("test.txt","a+");
if(trigger)
{
fwrite(s,1,n,fp);
}
fclose(fp);
}
int main()
{
turnOn();
write("First",5);
turnOff();
write("Second",6);
}

c语言需要用全局变量和全局函数来实现,在稍微大一些的项目中,将会产生难以维护,难以扩展,难以阅读的问题,而C++封装对象的方式就可以解决这些问题。

c语言在内存方面占用更小。在微型项目中编写更加方便。

第二章

一、基础题

  1. 下面标识符是合法的有哪些(BEF ) (涉及知识点: 2-5 标识符与关键字
    A.float
    B.ipad
    C.1button
    D. A#BC
    E.my_button
    F. button_1_ok
  2. 请给一个退出按钮命一个好变量名( C)(涉及知识点: 2-5 标识符
    A. 1button
    B. button1
    C.buttonQuit
    D.button_tuichu
  3. 下面整数常量合法的是( D)(涉及知识点: 2-6 常量
    A.078
    B.03UU
    C.0x9AHX
    D.0xFFAA00

二、提高题

  1. 下面程序输出结果是 (8)(涉及知识点: 2-6 常量的宏定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #define MA(x) x*(x-1)
    void main()
    {
    int a=1, b=2;
    cout << MA(1+a+b) << endl;
    } // 8
    //ma(1+a+b) = 1+a+b*(1+a +b-1)
    // = 1+1+2*(1+1+2-1)
    // = 8

第三章:

一、填空

  1. cout <<sizeof(char) << endl;   //1
    cout << sizeof(short) << endl; //2
    cout << sizeof(int) << endl; //4
    cout << sizeof(float) << endl; //4
    cout << sizeof(double) << endl; //8
    <!--2-->
  2. typedef struct{
    short Sunday = 0;
    short Monday = 1;
    short Tuesday = 2;
    short Wednesday = 3;
    short Thursday = 4;
    short Friday = 5;
    short Saturday = 6;
    } Week;
    Week w;
    cout << sizeof(w.Sunday); //2
    cout << sizeof(w) << endl; //14
    <!--3-->
  3. 分别写出bool 、int、 float、与“零值”比较,表达式返回值等于1的代码片段;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #设变量为flag
    ////////////////////////////////////////////////////////////////////////
    bool:
    if(flag)
    int:
    if(flag == 0)
    double:
    const double EPSINON = 0.00001;
    if ((flag >= - EPSINON) && (flag <= EPSINON))
    char *:
    if (flag == NULL)

第四五六章:

一、程序运行题

  1. 请说出下列问号处的结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
       char str[] = “Hello” ;
    char *p = str ;
    int n = 10;
    //请计算
    sizeof (str ) = ?
    sizeof ( p ) = ?
    sizeof ( n ) = ?
    void Func ( char str[100])
    {
    //请计算
    sizeof( str ) = ?
    }

答:
sizeof (str ) = 6
sizeof ( p ) = 4
sizeof ( n ) = 4
void Func ( char str[100])
{
sizeof( str ) = 4
}

  1. void GetMemory(char *p)
    {
        p = new char[100];
    }
    void Test(void)
    {
        char *str = NULL;
        GetMemory(str);
        strcpy(str, "hello world");
        printf(str);
    }
    
    1
    2
    3
    4
    5
        问运行Test 函数会有什么样的结果?
    **答:程序崩溃。因为GetMemory 并不能传递动态内存,
    Test 函数中的 str 一直都是 NULL。strcpy(str, "hello world");将使程序崩溃。**

    3.
    char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }
    1
    2
    3
    4
    请问运行Test 函数会有什么样的结果?
    **答:可能是乱码;因为GetMemory 返回的是指向“栈内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。**

    4.
    void GetMemory2(char **p, int num) { *p = new char[num]; } void Test(void) { char *str = NULL; GetMemory2(&str, 100); strcpy(str, "hello"); printf(str); }
    1
    2
    3
    4
    5
       请问运行Test 函数会有什么样的结果?
    **答:能够输出hello,但内存泄漏 **


    5.
    # include <string.h> void Test(void) { char *str = new char[100]; strcpy(str, "hello"); delete[ ] str; if (str != NULL) { strcpy(str,"world"); printf(str); } }
    1
    2
    3
    4
    5
       请问运行Test 函数会有什么样的结果?
    **答:篡改动态内存区的内容,后果难以预料,非常危险。因为 delete[ ]str;之后,str成为野指针(需要str = NULL;)if(str != NULL)语句不起作用。**

    ## 二、编程题
    1. char *strcpy(char *strDest, const char *strSrc)不调用C++/C 的字符串库函数,请编写函数 strcpy;
    char *strcpy(char *strDest, const char *strSrc); { assert((strDest!=NULL) && (strSrc !=NULL)); char *address = strDest; while( (*strDest++ = * strSrc++) != ‘\0’ ) ; return address ; }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 第七章:

    ## 一、编程题
    编写自定义类String 的构造函数、析构函数和赋值函数。
    已知类String 的原型为:
    class String
    {
    public:
    String(const char *str = NULL); // 普通构造函数
    String(const String &other); // 拷贝构造函数
    String(String&& other); // 移动构造函数
    ~String(void); // 析构函数
    String& operator= (const String& other); // 赋值函数
    String& operator=(String&& rhs)noexcept; // 移动赋值运算符
    private:
    char *m_data; // 用于保存字符串
    };
    请编写String 的上述几个函数。
    // String 的析构函数 String::~String(void) { if (m_data != NULL) { delete[] m_data; } }

// String 的普通构造函数
String::String(const char *str)
{
if (str == NULL)
{
m_data = new char[1];
if (m_data != NULL)
{
*m_data = ‘\0’;
}
else
{
exit(-1); // new有可能失败,失败后返回错误退出, 最好能有日志
}
}
else
{
int length = strlen(str);
m_data = new char[length + 1];
if (m_data != NULL)
{
*m_data = ‘\0’;
}
else
{
exit(-1); // new有可能失败,失败后返回错误退出, 最好能有日志
}
strcpy(m_data, str);
}
}
// 拷贝构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length + 1];
if (m_data != NULL)
{
*m_data = ‘\0’;
}
else
{
exit(-1); // new有可能失败,失败后返回错误退出, 最好能有日志
}
strcpy(m_data, other.m_data);
}

String::String(String&& other)
{
if (other.m_data != NULL)
{
m_data=other.m_data;
other.m_data = NULL;
}
}

// 赋值函数
String& String::operator= (const String &other)
{
// 检查自赋值
if (this == &other)
return *this;
// 释放原有的内存资源
delete[] m_data;
// 分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length + 1];
if (m_data != NULL)
{
*m_data = ‘\0’;
}
else
{
exit(-1); // new有可能失败,失败后返回错误退出, 最好能有日志
}
strcpy(m_data, other.m_data);
// 返回本对象的引用
return *this;
}

String& String::operator=(String&& rhs)noexcept
{
if (this != &rhs)
{
delete[] m_data;
m_data = rhs.m_data;
rhs.m_data = NULL;
}
return *this;
}
```

广义的IO模型:阻塞I/O->非阻塞忙轮询I/O->select/epoll无差别轮询代理-> epoll

#b计算机基础/c_计算机系统/b_linux系统/补充
2020年4月8日 下午3:25

总结:

  1. 下面这篇文章其实写的非常好,我反复多了多次才明白其中的关键:
    1. 首先,你需要理解流的概念:
      1. 其实我们在server定义socket的时候server = socket(AF_INET, SOCK_STREAM),这个定义会产生一个流,只不过在没有client进行连接的时候,这个流中的内容为空而已。
      2. 在代码中我就一直疑惑:流数组是从哪里来的?
        1. 上面我们知道定义socket会产生一个流(“缓冲区空”),当client连接server的时候,这个流中就会有信息在传递(“缓冲区非空”)
        2. 这里比较特殊的一个地方是:socket流中的信息也是一个流,代表着不同client与server的链接。
        3. 当有n个client与server进行连接的时候,加上原本socket,在当前进程中,就会有n+1个流对象
    2. 整个的IO模型,其实就是信息在流上进行一个个进行传递的过程。有些流中传递的还是流,有些流中传递的时候真正的byte数据。
    3. IO模型与网络IO模型的关系:
      1. 我目前见过做多的就是网络IO模型,当你把client与server之间的socket连接理解成一个流的时候,你就会发现其实就是IO模型,不要因为加上了网络就觉得难以理解
      2. IO模型的基础操作单位是流,具体你是什么流(文件,socket,pipe)对于IO模型来说其实是透明的,流这个抽象就能把具体的子类对上层隐藏。

python代码实现:

python基础-io模型、阻塞、非阻塞、io多路复用_Python_金丙坤-CSDN博客
io模型、阻塞、非阻塞、io多路复用这四种对应的client,server代码!

概念的理解:

补充:Epoll之ET、LT模式_网络_feitianxuxue的专栏-CSDN博客

  1. 在使用epoll时,在函数 epoll_ctl中如果不设定,epoll_event 的event默认为LT(水平触发)模式。

  2. LT模式

    1. 使用LT模式意味着只要fd处于可读或者可写状态,每次epoll_wait都会返回该fd,这样的话会带来很大的系统开销,且处理时候每次都需要把这些fd轮询一遍,如果fd的数量巨大,不管有没有事件发生,epoll_wait都会触发这些fd的轮询判断。
  3. ET模式:

    1. 在ET模式下,当有事件发生时,系统只会通知你一次,即在调用epoll_wait返回fd后,不管这个事件你处理还是没处理,处理完没有处理完,当再次调用epoll_wait时,都不会再返回该fd,这样的话程序员要自己保证在事件发生时要及时有效的处理完该事件。
  4. 首先我们来定义流的概念

    1. 一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。
    2. 不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
  5. 之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

    • 阻塞。阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。
    • 非阻塞轮询。接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”
      • 很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。
      • 大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。
  6. 为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I_O事件解释清楚。缓冲区的引入是为了减少频繁I_O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。
    假设有一个管道,进程A为管道的写入方,B为管道的读出方。

    • 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
    • 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
    • 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
    • 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。
    • 这四个情形涵盖了四个I_O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。 __这四个I_O事件是进行阻塞同步的根本 。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。
  7. 然后我们来说说阻塞I/O的缺点

    1. 阻塞I_O模式下,一个线程只能处理一个流的I_O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
  8. 于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):

    1
    2
    3
    4
    5
    6
    while true {
    for I in stream[]{ #一直轮询
    if I has data
    read until unavailable
    }
    }
    • 我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I_O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I_O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。
  9. 为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I_O事件,在空闲的时候,**_会把当前线程阻塞掉**_,当有一个或多个流有I_O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

    1
    2
    3
    4
    5
    6
    7
    while true {
    select(streams[]) # 增加的这步
    for I in streams[] {
    if I has data
    read until unavailable
    }
    }
    • 于是,如果没有I_O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I_O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
    • 但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次
  10. 说了这么多,终于能好好解释epoll了

    • Epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I_O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I_O事件的流的个数,也有认为O(1)的[更新 1])
    • epoll实现机制:
      • 使用epoll的时候,内核会维护一个就绪的链表,这个链表里面的东西就是”那些发生了I/O事件的流”
      • 而这些文件描述符是如何被添加到就绪链表里面的呢?是通过注册的那些回调函数实现的。
      • 而怎么知道这些文件描述符上发生的是什么事件呢?这个就需要我们自己在代码里面去判断了。epoll这个函数返回的只是那些发生了事件的文件描述符。 因为这些事件是你自己给这个文件描述符的,所以你可以通过代码判断发生了什么I/O事件。
      • 于是就有”epoll只会把哪个流发生了怎样的I/O事件通知我们”。
    • 在讨论epoll的实现细节之前,先把epoll的相关操作列出[更新 2]:
      1. epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
      2. epoll_ctl (epoll_add_epoll_del的合体),往epoll对象中增加_删除某一个流的某一个事件
      3. 比如
      4. epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);有缓冲区内有数据时epoll_wait返回
      5. epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);缓冲区可写入时epoll_wait返回
      6. epoll_wait(epollfd,…)等待直到注册的事件发生
      7. (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。
    • 一个epoll模式的代码大概的样子是:
      1
      2
      3
      4
      5
      6
      while true {
      active_stream[] = epoll_wait(epollfd) # 关键步骤
      for I in active_stream[] {
      read or write till unavailable
      }
      }
  • 限于篇幅,这里只讲了揭示原理性的东西,至于epoll的使用细节,请参考man和google,实现细节,请参阅linux kernel source。
  • [更新1]: 原文为O(1),但实际上O(k)更为准确
  • [更新2]: 原文所列第二点说法让人产生EPOLLIN/EPOLLOUT等同于“缓冲区非空”和“缓冲区非满”的事件,但并非如此,详细可以Google关于epoll的边缘触发和水平触发。

2020年4月8日 上午11:51

专题知识点3:constconst是存储在.text的常量区

new/delete与malloc/free的区别是什么

  1. 首先,new_delete是C++的关键字,而malloc_free是C语言的库函数,
  2. malloc不会调用构造函数和析构函数:
    1. 需要给定申请内存的大小,返回的指针需要强转。
  3. new会调用构造函数
    1. 不用指定内存大小,返回的指针不用强转。

什么是memory leak,也就是内存泄漏

  1. 内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的分类:内存泄漏的常见原因

  1. 堆内存泄漏 (Heap leak)。
    1. 对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
  2. 系统资源泄露(Resource Leak)。
    1. 主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
  3. 没有将基类的析构函数定义为虚函数。
    1. 当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
  4. 专题知识点1:指针专题smart_ptr也可以引起内存泄漏

C++的内存管理是怎样的?

  1. 在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
  2. 代码段:
    1. 包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  3. 数据段:
    1. 存储程序中已初始化的全局变量和静态变量
  4. Bss 段:
    1. 存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
  5. 堆区:
    1. 调用new_malloc函数时在堆区动态分配内存,同时需要调用delete_free来手动释放申请的内存。
  6. 映射区:
    1. 存储动态链接库以及调用mmap函数进行的文件映射
  7. 栈:
    1. 使用栈空间存储函数的返回地址、参数、局部变量、返回值

函数在main函数执行前先运行

  1. 在 .text之前:data段的内容:
    1. 全局对象的构造函数会在main 函数之前执行。
    2. 一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前,而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
  2. 在 .text之前:.bss段的内容
    1. 进程启动后,要执行一些初始化代码(如设置环境变量等),然后跳转到main执行。全局对象的构造也在main之前。
  3. 通过关键字attribute,让一个函数在主函数之前运行,进行一些数据初始化、模块加载验证等。

Main函数执行之前,主要就是初始化系统相关资源:

C语言深度总结全面认识main函数之前运行代码 - 简书

  1. 设置栈指针
  2. 在 .text之前:data段的内容:
    1. 初始化static静态和global全局变量
  3. 在 .text之前:.bss段的内容
    1. 将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等
  4. .text段:
    1. 将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数

2020年4月8日 下午5:57

关键:理解了buffer的作用

其实就是有一个buffer用作缓存(事件(消息)队列),当动作发生的时候,我并不执行,而是将它扔到一个buffer里,让另外的处理线程去执行,这个处理线程就会不间断的执行buffer里的任务,如果当前的动作发生了阻塞,那么处理线程立刻再把它放到这个buffer中(注册一个回调到事件循环中),找下一个进行执行,直到buffer为空

事件驱动编程模型

IO模式和IO多路复用 - ZingpLiu - 博客园

3.1论事件驱动

  1. 通常,我们写服务器处理模型的程序时,有以下几种模型
    1. (1)每收到一个请求,创建一个新的进程,来处理该请求;
    2. (2)每收到一个请求,创建一个新的线程,来处理该请求;
    3. (3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
  2. 上面的几种方式,各有千秋:
    1. 第(1)中方法,由于创建新的进程:实现比较简单,但开销比较大,导致服务器性能比较差。
    2. 第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
    3. 第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。
  3. 综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式。

3.2 看图说话讲事件驱动模型

在UI编程中,常常要对鼠标点击进行相应,首先如何获得鼠标点击呢?

  1. 方式一:创建一个线程,该线程一直循环检测是否有鼠标点击,那么这个方式有以下几个缺点
    1. CPU资源浪费,可能鼠标点击的频率非常小,但是扫描线程还是会一直循环检测,这会造成很多的CPU资源浪费;如果扫描鼠标点击的接口是阻塞的呢?
    2. 如果是堵塞的,又会出现下面这样的问题,如果我们不但要扫描鼠标点击,还要扫描键盘是否按下,由于扫描鼠标时被堵塞了,那么可能永远不会去扫描键盘;
    3. 如果一个循环需要扫描的设备非常多,这又会引来响应时间的问题;
        所以,该方式是非常不好的。
  2. 方式二:就是事件驱动模型
    目前大部分的UI编程都是事件驱动模型,如很多UI平台都会提供onClick()事件,这个事件就代表鼠标按下事件。事件驱动模型大体思路如下:
    1. 有一个事件(消息)队列;
    2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
    3. 有个循环,不断从队列取出事件,根据不同的事件,调用不同的函数,如onClick()、onKeyDown()等;
    4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
  3. 事件驱动编程是一种网络编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。
  4. 让我们用例子来比较和对比一下单线程、多线程以及事件驱动编程模型。下图展示了随着时间的推移,这三种模式下程序所做的工作。这个程序有3个任务需要完成,每个任务都在等待I_O操作时阻塞自身。阻塞在I_O操作上所花费的时间已经用灰色框标示出来了。
    • 在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。
    • 在多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的bug。
    • 在事件驱动版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I_O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I_O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。
  5. 当我们面对如下的环境时,事件驱动模型通常是一个好的选择:
    • 程序中有许多任务,而且…
    • 任务之间高度独立(因此它们不需要互相通信,或者等待彼此)而且…
    • 在等待事件到来时,某些任务会阻塞。
    • 当应用程序需要在任务间共享可变的数据时,这也是一个不错的选择,因为这里不需要采用同步处理。
    • 网络应用程序通常都有上述这些特点,这使得它们能够很好的契合事件驱动编程模型。

2020年4月6日 下午11:30

strcpy和strlen

  1. Strcpy是字符串拷贝函数,原型:
    1. char *strcpy(char* dest, const char *src);
    2. 从src逐字节拷贝到dest,直到遇到’\0’结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是strncpy函数。
  2. Strlen函数是计算字符串长度的函数,返回从开始到’\0’之间的字符个数。

全局变量和静态全局变量的区别

关于int、short int、long int、long long 的区别

关于int、short int、long int、long long 的区别_C/C++_Adrianna的专栏-CSDN博客

  1. 在标准中,并没有规定long一定要比int长,也没有规定short要比int短。
  2. 标准是这么说的:长整型至少和整型一样长,整型至少和短整型一样长。
    1. short int 2个字节
    2. int 2/4字节
    3. long 4/8字节
    4. long long 8字节

从双刃剑的角度去总结
C++中类的(static)静态成员变量与(static)静态成员函数_C/C++_年少轻狂,幸福时光-CSDN博客
static能怎样,不能怎样

typedef vs define

C typedef | 菜鸟教程

  • #define 是 C 指令,用于为各种数据类型定义别名,与 typedef 类似,但是它们有以下几点不同:
  • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 是由编译器执行解释的,#define语句是由预编译器进行处理的。

++i和i++的实现

  1. ++i 实现:
    1
    2
    3
    4
    5
    int&  int::operator++()
    {
    *this +=1
    return *this
    }
  2. i++ 实现:
    1
    2
    3
    4
    5
    6
    const int int::operatorint
    {
    int oldValue = *this
    ++(*this);
    return oldValue;
    }

类型转换

C++进阶–类型转换,你看我就够了 - 简书

  1. C++风格的强制转换其他的好处是
    1. C++中风格是static_cast(content)。C++风格的强制转换其他的好处是,它们能更清晰的表明它们要干什么。程序员只要扫一眼这样的代码,就能立即知道一个强制转换的目的。
  2. reinterpret_cast:重解析类型转换
    • 可以用于任意类型的指针之间的转换,对转换的结果不做任何保证
  3. dynamic_cast:
    • 这种其实也是不被推荐使用的,更多使用static_cast,dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常
    • 可以动态的来判断当前对象的真实类型
  4. const_cast:
    • 对于未定义const版本的成员函数,我们通常需要使用const_cast来去除const引用对象的const,完成函数调用。
    • 另外一种使用方式,结合static_cast,可以在非const版本的成员函数内添加const,调用完const版本的成员函数后,再使用const_cast去除const限定。
  5. static_cast:
    • 能使用隐式转换的地方,均可以使用static_cast转换
    • 如果类型不兼容,使用static_cast编译检查,会报错,不用等到运行阶段

说说你了解的RTTI

运行时类型检查,在C++层面主要体现在dynamic_cast和typeid,VS中虚函数表的-1位置存放了指向type_info的指针。对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info

struct和class的区别

  1. 默认的继承访问权:【继承下】
    1. class默认的是private,strcut默认的是public。
    2. 当然,到底默认是public继承还是private继承,取决于子类而不是基类。意思是,struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。
  2. 成员变量默认访问权限:【非继承下】
    1. struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
  3. 有代码实验:【C++】struct和class的区别_C/C++_alidada_blog的博客-CSDN博客

C++中可以定义引用数据成员吗?

C/C++ - 类中成员变量是引用_C/C++_lazyq7的博客-CSDN博客

  1. 所有的引用自带要求,并不一定是在类中:
    1. 引用在定义时必须初始化,否则编译时便会报错。
  2. 根据1,我们可以推得在类中,如何使用引用数据成员:
    1. 构造函数:
      1. 引用类型的成员变量的类,不能有缺省构造函数
      2. 不能直接在构造函数里初始化,必须用到初始化列表
        1. 默认构造函数没有对引用成员提供默认的初始化机制,也因此造成引用未初始化的编译错误。

2020年4月6日 下午11:00

以下四行代码的区别是什么? const char * arr = “123”; char * brr = “123”; const char crr[] = “123”; char drr[] = “123”;

  1. const char * arr = "123”;
    • 字符串123保存在常量区
    • const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
  2. char * brr = "123";
    • 字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改”123”的值
  3. const char crr[] = “123”;
    • 这里123本来是在栈上的,但是编译器可能会做某些优化,将其放到常量区
  4. char drr[] = “123”;
    • 字符串123保存在栈区,可以通过drr去修改

Const 修饰指针变量有以下三种情况

  • A: const 修饰指针指向的内容,则内容为不可变量。
    1
    2
    const int *p = 8;
    //则指针指向的内容 8 不可改变。简称左定值,因为 const 位于 * 号的左边。
  • B: const 修饰指针,则指针为不可变量。
    1
    2
    3
    4
    5
    6
    int a = 8;
    int* const p = &a;
    *p = 9; // 正确
    int b = 7;
    P = &b; // 错误
    //对于 const 指针 p 其指向的内存地址不能够被改变,但其内容可以改变。简称,右定向。因为 const 位于 * 号的右边。
  • C: const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
    1
    2
    3
    int a = 8;
    const int * const p = &a;
    //这时,const p 的指向的内容和指向的内存地址都已固定,不可改变。

C++里是怎么定义常量的?常量存放在内存的哪个位置?

  1. C++ 常量 | 菜鸟教程
    1. 常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量
    2. 常量可以是任何的基本数据类型,可分为整型数字、浮点数字、字符、字符串和布尔值。
    3. 常量就像是常规的变量,只不过常量的值在定义后不能进行修改。
  2. 在 C++ 中,有两种简单的定义常量的方式:
    1. 使用 #define 预处理器。
    2. 使用 const 关键字。
  3. 常量在C++里的定义就是一个top-level const加上对象类型,常量定义必须初始化。
    1. 对于局部对象,常量存放在栈区
    2. 对于全局对象,常量存放在全局/静态存储区。
    3. 对于字面值常量,常量存放在常量存储区。
  4. 代码段:
    1. 包括只读存储区和文本区,其中只读存储区存储字符串常量(const就存在这里),文本区存储程序的机器代码。
  5. C++内存分配方式详解(堆、栈、自由存储区、全局/静态存储区和常量存储区)_C/C++_那年聪聪-CSDN博客
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int  a=0;   全局初始化区 .data   

    char *p1;   全局未初始化区 .bss  
    int  main()    
    {    
      int  b; //栈    
      char  s[]="abc"//栈    
      char  *p2; //栈    
      char  *p3="123456"//123456/0在常量区 .text中的_const,p3在栈上。    

      static int c =0//全局(静态)初始化区  .data  
      p1 =  (char  *)malloc(10);  //分配得来得10和20字节的区域就在堆区
      p2  = (char  *)malloc(20);       
      strcpy(p3,"123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"  优化成一个地方。    
    }
  6. 堆和栈究竟有什么区别?
    1. C++内存分配方式详解(堆、栈、自由存储区、全局/静态存储区和常量存储区)_C/C++_那年聪聪-CSDN博客

Const修饰成员函数的目的是什么?

Const修饰的成员函数表明函数调用不会对对象做出任何更改,事实上,如果确认不会对对象做更改,就应该为函数加上const限定,这样无论const对象还是普通对象都可以调用该函数。

同时定义了两个函数,一个带const,一个不带,会有问题吗?

  1. 不会,这相当于函数的重载。
  2. 原因是:
    1. 按照函数重载的定义,函数名相同而形参表有本质不同的函数称为重载。在类中,由于隐含的this形参的存在,const版本的function函数使得作为形参的this指针的类型变为指向const对象的指针,而非const版本的使得作为形参的this指针就是正常版本的指针。
    2. 此处是发生重载的本质。重载函数在最佳匹配过程中,对于const对象调用的就选取const版本的成员函数,而普通的对象调用就选取非const版本的成员函数。
  3. (注:this指针是一个const指针,地址不能改,但能改变其指向的对象或者变量)
  4. C++ 学习之函数重载、基于const的重载_C/C++_guiyinzhou的专栏-CSDN博客

2020年4月6日 下午10:49

C语言是怎么进行函数调用的?

C++栈调用过程:C++自带功能

C++函数栈空间的最大值

默认是1M,不过可以调整

C语言参数压栈顺序?

从右到左

C++如何处理返回值?

生成一个临时变量,把它的引用作为函数参数传入函数内。