ThinkNotes

Simple is not easy | 化繁为简,知易行难

0%

前言

MFC(Microsoft Foundation Classes)是微软在win32 API上,用C++封装的GUI框架,在现在,MFC相比其他的GUI框架有些过时,可以参考:
很多人说 C++ 的 MFC 已经过时了,那新入门的人到底应该学什么?
不同环境的选择:

  • 跨平台: QT
  • C#: WPF
  • Web:React,Vue,Electron

既然如此,为何本文用MFC?
1.部分功能从老MFC项目移植,且VS环境能快速上手
2.技术本身不会过时,过时的是应用场景,GUI回调式的交互机制,以及Win32线程和进程的使用都是通用的技术。这是写本文的原因

本文源码:cursorhu/myMFCForAutoRWTest

GUI界面:
1

初识MFC项目

VS新建MFC项目,例如“myMFC”,目录结构如下
2
myMFC.cpp是VS自动创建的MFC项目入口,其主要功能是:创建一个窗口实例,注册会话对象(Dialog)
界面的交互一定是分层的

  • 对用户的是控件层,即各种按钮,输入输出框等可见可操作的东西
  • 处理数据的是逻辑层,例如从输入框输入,底层保存该字符串,点击运行,底层开始执行对应函数

在MFC中,会话对象就是处理底层逻辑的类对象,其方法定义在myMFCDlg.cpp
也是开发的主要内容

阅读全文 »

Markdown语法

标题

#
##
### 

无序列表

- line 
或者
* line

有序列表

1
2
1. line
2. line

转义字符

有的文字或代码和markdown解析有冲突
如$, @等
在这些字符前加转义字符即可:$, @

tab缩进

阅读全文 »

概述

I/O多路复用:通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

IO多路复用适用如下场合:

  • 当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
  • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。
  • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
  • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
  • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

select实现

逻辑时序:
1
具体实现:
2

fd_set(监听的端口个数):32位机默认是1024个,64位机默认是2048。

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间
(2)注册回调函数__pollwait
(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
(8)把fd_set从内核空间拷贝到用户空间。

阅读全文 »

1.简介

Tinyhttpd是一个C + CGI实现的简单http server,适合初学者学习。代码许可协议:GPL,copyright 1999, by J. David Blackstone.
本文对Tinyhttp稍作注释和改动,验证并理解其主要流程, 本文源码:
Github: cursorhu/myTinyHttpd

2.背景知识

TCP套接字的通信流程

网络协议栈的核心是TCP/IP协议,HTTP本质上是对TCP的应用层封装,要理解HTTP服务程序,首先要理解TCP层的通信机制,在Linux环境中TCP采用socket接口通信,流程如下图
image-20221212145149039
关于Linux网络编程相关知识,参考《Linux网络编程-第二版》
TinyHttpd实现服务端的流程。

HTTP的请求方式

参考:
浅谈HTTP中GET、POST用法以及它们的区别
99%的人都理解错了HTTP中GET与POST的区别
理解以下几点:

  • GET,POST,PUT,DELETE是http层对数据操作的封装,底层本质还是TCP的read/write过程
  • http server处理请求的基本流程:读取-拆解-处理-封装-回写,拆解和封装的就是http层的请求和数据格式,处理是指TCP层能理解的数据。就像快递退货时的流程:取件-拆包-查看-装包-寄出

CGI的时代背景

参考:CGI是什么

阅读全文 »

基础操作

拉取和同步

git clone http://xxx.xxx.git //http方式, 从远程clone仓库,注意这种方式只clone master到本地,本地要其他分支要手动checkout branchname.
git pull //拉取远程分支
git branch //查看本地
git branch -a //查看远程和本地
git checkout xxxbranch //本地切到某分支
git checkout xxx/xxx //仅拉取部分目录或文件

查看日志

1
2
3
git log //回车下一行,空格下一页,q退出
git log --pretty=oneline //单行显示每个commit,用于查看大量提交
git log --author=“author” //查看某人的提交

推送到远程

git add -A //推送所有修改到本地仓库
git commit -m "change logs" //提交到本地仓库(记录修改信息)
git push //推送本地分支到远程的同名分支,需要先关联
git push origin <本地分支名> //推送本地分支到远程同名分支
git push origin <本地分支名>:<远程分支名> //推送本地分支到远程指定分支

加tag/删tag

git tag -a TAGNAME -m "TAG LOG" //加tag
git push origin TAGNAME //推送tag到远程
git tag -d TAGNAME //删除本地tag
git push origin :refs/tags/TAGNAME //删除远程tag

创建/删除/修改分支

阅读全文 »

string

find()

查找指定字符串的位置(下标)

string中find()返回值是字母在母串中的位置(下标记录),如果没有找到,那么会返回一个特别的标记npos。(返回值可以看成是一个int型的数)

//find函数返回类型 size_type
string s("1a2b3c4d5e6f7jkg8h9i1a2b3c4d5e6f7g8ha9i");
int position;
//find 函数 返回jk 在s 中的下标位置
position = s.find("jk");
if (position != s.npos)  //如果没找到,返回一个特别的标志c++中用npos表示
{
    printf("position is : %d\n" ,position);
}
else
{
    printf("Not found the flag\n");
}

##查找某字符首次出现,或最后出现的位置
find_first_of() 和 find_last_of()返回子串出现在母串中的首次出现的位置,和最后一次出现的位置
查找上面示例的’c’的下标:

flag = "c";
position = s.find_first_of(flag);
printf("s.find_first_of(flag) is :%d\n",position);
position = s.find_last_of(flag);
printf("s.find_last_of(flag) is :%d\n",position);

image-20221208171122674

查找某给定位置后的子串的位置

//从字符串s 下标5开始,查找字符串b ,返回b 在s 中的下标
position=s.find("b",5);
cout<<"s.find(b,5) is : "<<position<<endl;
阅读全文 »

0.前言

相信大家面试都逃不开设计模式话题,本节将阐述面试中的最常用的设计模式(单例模式),从分类,线程安全,不基于C++11标准的角度与基于C++11标准的角度,有哪些解决线程安全的单例模式方案,相信认真看完本篇文章,在以后面试中就不用担忧了。

众所周知的单例:
在一般书籍中或者大家比较是熟知的单例模式是下面这样:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if (p == nullptr)
        p = new singleton();
    return p;
}

这是一个非常简单的实现,将构造函数声明为private或protect防止被外部函数实例化,内部有一个静态的类指针保存唯一的实例,实例的实现由一个public方法来实现,该方法返回该类的唯一实例。

当然这个代码只适合在单线程下,当多线程时,是不安全的。考虑两个线程同时首次调用instance方法且同时检测到p是nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。

2.懒汉与饿汉

单例分为两种实现方法:

懒汉:第一次用到类实例的时候才会去实例化,上述就是懒汉实现。
饿汉:单例类定义的时候就进行了实例化。

这里也给出饿汉的实现:

阅读全文 »

Linux虚拟内存空间分布

(1)虚拟内存空间与物理内存:
带MMU控制器的CPU支持将物理内存以分页的方式,细粒度的动态分配给进程,使每个进程只看得到这个虚拟的内存空间,每个进程认为自己可以访问整个内存空间。进程根本不知道其访问的某个内存页的实际物理地址,也许在SDRAM上,或者硬盘的交换分区上。

进程的虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用。每个进程都拥有一套属于它自己的页表。

(2)下面讨论用户进程能看到什么样的虚拟内存空间:

以32位系统为例,CPU可寻址4GB的内存空间。此时虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分:

  • 将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。
  • 将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间

因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

image-20221205115648795

注意:

  • 内核可见的内存空间只有全局的1GB; 用户进程可见的内存空间包括该进程独有的3GB空间,和全局内核的1GB;
  • 用户进程虽然可见内核空间的1GB,但不可直接访问,要通过系统调用(或中断等方式),涉及上下文切换;
  • 当进程访问内核空间时,称为“进入内核态”,返回时称为“进入用户态”;
  • 内核空间分布在虚拟内存空间的高地址,用户空间在低地址
阅读全文 »

本文将从以下几个方面来阐述信号:

(1) 信号的基本知识
(2) 信号生命周期与处理过程分析
(3) 基本的信号处理函数
(4) 保护临界区不被中断
(5) 信号的继承与执行
(6) 实时信号中锁的研究

第一部分: 信号的基本知识

1.信号本质:

信号的本质是软件层次上对中断的一种模拟。它是一种异步通信的处理机制,事实上,进程并不知道信号何时到来。

2.信号来源

(1)程序错误,如非法访问内存
(2)外部信号,如按下了CTRL+C
(3)通过kill或sigqueue向另外一个进程发送信号

3.信号种类

信号分为可靠信号与不可靠信号,可靠信号又称为实时信号,非可靠信号又称为非实时信号。
信号代码从1到32是不可靠信号,不可靠信号主要有以下问题:
(1)每次信号处理完之后,就会恢复成默认处理,这可能是调用者不希望看到的
(2)存在信号丢失的问题
现在的Linux对信号机制进行了改进,因此,不可靠信号主要是指信号丢失
信号代码从SIGRTMIN到SIGRTMAX之间的信号是可靠信号。可靠信号不存在丢失,由sigqueue发送,可靠信号支持排队。

可靠信号注册机制:
内核每收到一个可靠信号都会去注册这个信号,在信号的未决信号链中分配sigqueue结构,因此,不会存在信号丢失的问题。

阅读全文 »

1.资源与内存分配

资源的概念:资源是数量有限且对系统正常运转具有一定作用的元素。比如,内存,文件句柄,网络套接字(network sockets),互斥锁(mutex locks)等等
对于进程,这些资源都作为某种数据结构存储在内存中。
程序运行需要分配内存来管理以上资源,内存分配可以分为三类:

  • 静态分配:如创建一个进程执行某段代码,需要加载该代码的代码段,数据段等数据到内存中,其中数据段包含已初始化的全局数据,可以称为是静态的内存分配
  • 自动分配:进程内函数的调用和返回,以及其内部的局部变量创建和销毁,对应该进程高地址的入栈出栈,这个是操作系统自动处理的,无需应用程序控制
  • 动态分配:静态数据和堆栈之前的空间(称为堆),可由应用程序动态分配,同时,也必须由应用程序释放。所谓的内存的动态分配与释放,通常讨论的是这种情况

以32位Linux环境的应用程序为例,每个进程可见的(虚拟)内存分布如下,C/C++常用的malloc/free, new/delete对应的内存分配释放都在.heap段内
image-20221208165846274

2.动态内存管理的缺陷

我们在使用资源时必须严格遵循的步骤是:

  1. 获取资源
  2. 使用资源
  3. 释放资源

代码形式:

void UseResources()    
{  
    // 获取资源1  
    // ...  
    // 获取资源n  
     
    // 使用这些资源  
     
    // 释放资源n  
    // ...  
    // 释放资源1  
} 

当代码量和复杂度达到一定程度,这种手动资源管理容易出错,且难以避免
例如C++使用new和delete时可能发生的一些错误是:

阅读全文 »