Shunlqing's Blog

触动、反思,然后重新出发


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

Hexo Start

发表于 2018-05-18

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

非阻塞式IO

发表于 2018-05-18 | 分类于 技术笔记

一、概述

阻塞与非阻塞的概念

在用户进程或者线程进行系统调用进入内核的时候,如果不能马上满足条件,内核对调用者采取不同的行为,分为“阻塞”和”非阻塞“两种。”阻塞“表示如果系统调用不能马上获得满足而返回,就将调用者(进程或线程)挂起,等到满足条件,内核执行完系统调用,重新恢复调用者。”非阻塞“表示如果系统调用不能满足,则立即返回,并设置错误信息以告知调用者,该系统调用因何不能满足。

从以上看,”阻塞“虽然使用比较简单,但是很被动,会出现”永久阻塞“而导致无法处理的问题,设想一下,客户端进程阻塞等待接受服务器的消息,但是这个服务器因为某种原因崩溃了,如此客户端就会永久阻塞。相反,”非阻塞“技术给了程序员更大的灵活性,只要能够合理处理各种情况便能够更好的提高效率。

总之,非阻塞调用相对与阻塞调用有几个好处:

  • 不会出现“永久阻塞”(设置超时也可解决永久阻塞问题)
  • 同时监听多个IO复用的程序模型,如果使用阻塞调用,可能出现顾此失彼的情况。即,系统调用阻塞在某个IO上时,可能有另一个IO已经就绪,而它不能被及时响应。

也因为如此,使用非阻塞技术要体现这些优势。

网络编程背景下的非阻塞式IO

可能阻塞的套接字调用分为以下四类:

  • 输入操作(read、readv、recv、recvfrom、recvmsg五个函数)
  • 输出操作(write、writev、send、sendto、sendmsg五个函数)
  • 接受外来连接,accept函数
  • 发起外出连接,用于TCP的connect函数

二、非阻塞读和写

UNP给出一个数据,echo服务下的客户端不同版本str_cli函数,测试从一个Solaris客户主机想RTT为175毫秒的一个服务器主机复制2000行文本,所花时间的不同:

  • 354.0s,停等版本
  • 12.3s,select+阻塞式IO版本
  • 6.9s, select+非阻塞IO版本
  • 8.7s, fork版本(多进程)
  • 8.5s,多线程版本
    从这个数据可以看出:
  • 一、IO复用模型的使用确实大大加快了IO的处理
  • 二、IO复用模型搭配非阻塞技术可以再提高效率(但是会提高编程的复杂性)

UNP推荐:相对于select+非阻塞IO,最好使用效率差不多、但是编程更简单的多进程模型。

对UNP书上str_cli函数的select+非阻塞式IO的注解

本版本需要做的工作:

  • 维护两个缓冲区;
  • 使用selectIO复用,监听并处理四个IO;
  • 对所有监听的IO都设置成非阻塞IO;
  • 恰当处理终止条件,当服务器终止连接或者客户端标准输入得到EOF;

动态性强,但是编程要考虑的情况多。UNP提供了效率差不多,但是编程简单的fork版本。该版本做的工作较少:

  • fork出子进程,负责阻塞等待套接字可读,并将收到的消息输出到标准输出;
  • 父进程负责阻塞等待标准输入可读,并将收到的消息写入套接字;

三、非阻塞connect

Linux进程和线程

发表于 2018-05-18 | 分类于 操作系统 , 技术笔记

问题

  • Linux系统中,进程和线程的区别?
  • Linux系统中,进程和线程的效率差别大不大,如何验证?

C/C++数组名和指针的关系

发表于 2018-05-18 | 分类于 编程语言 , 技术笔记

数组名不是指针!!!

  • 数组名指代的是数组整个实体结构。
  • 数组名可以外延成指代实体的指针,而且是一个指针常量。
  • 指向数组的指针,仅仅是数组第一个元素的地址。
  • a[i]内部翻译为*(a+i)
  • 对数组名取地址,&array+1,其步长是整个数组,与&(array+1),步长是一个数组元素不同

数组名退化为指针

在形参为数组的情况下,数组名会退化为指针,在函数内部,实际上是一个指针。

1
2
3
4
void foo(int array[])
{
cout << sizeof(array) << endl; //输出为8,为一指针大小,不是数组的大小
}

sizeof是个操作符,不是函数

sizeof操作符:求得是对象或类型的大小,这里的“大小”指的是“所占内存空间的实际大小”。

数组名代表的是整个数组结构实体,所以求得的是整个数组实体的内存大小。

指针结构实体本身,表示“一个指针结构”占用的内存大小,64位系统为8字节。

1
2
3
4
5
int array[10]; 
sizeof(array); // 4 * 10 = 40字节

int * p;
sizeof(p); // 8字节

高级IO函数

发表于 2018-05-18 | 分类于 技术笔记

一、概述

讨论几个问题:

  • 为避免函数调用不可预期的永久阻塞,可以对IO操作设置超时,设置超时有哪些方法?
  • 最基本的读写函数read/write,及其各种变体(send, recv, recvmsg….)是什么样的关系?
  • 如果仅仅想查看套接字接收缓冲区的数据而不读取,要怎么做?

二、为套接字设置超时

抛出各种具体方法:

  • 信号中断:alarm函数 + SIGALRM信号(缺点:干扰程序正常alarm的使用,在多线程程序中使用信号很困难)
  • select保安:利用select可以设置超时,让select代为阻塞等待在IO上。
  • 套接字选项:SO_RCVTIMEO和SO_SNDTIMEO(不能用于connect设置超时)

三、读写函数的各种变体

基本read/write及其三种变体send/recv、readv/writev和recvmsg/sendmsg

简要对比:

  • send/recv对比read/write多了一个flags参数,可以传给内核;
  • readv/writev对比read/write,后者是单个缓冲区的读写,前者则可以支持在单个系统调用中实现多个缓冲区的读写,称为“分散读”和“集中写”。
  • recvmsg/sendmsg是最通用的函数,它集中了前面集中的所有特性。但是这组只能在套接字描述符中使用。
  • sendto/recvfrom一般只在UDP套接字中使用。

send/recv

1
2
3
#include <sys/socket.h>
ssize_t recv(int sockfd, void* buff, size_t bytes, int flags);
ssize_t send(int sockfd, const void* buff, size_t bytes, int flags);

flags——值参数

  • MSG_DONTROUTE: 发送操作无须路由,仅限发送操作。表示目的主机在某个直接连接的本地网络上。
  • MSG_DONTWAIT:单次操作“非阻塞”。
  • MSG_OOB:对于send,表示即将发送的数据是带外数据(对于TCP只有一个字节);对于recv表示,即将读入的是带外数据而不是普通数据。
  • MSG_PEEK:表示仅查看可读取的数据,不作读取。适用于recv和recvmsg。
  • MSG_WAITALL:表示尚未读到请求数目的字节之前不让一个读操作返回。意外情况:a. 捕获一个信号;b.连接被终止;c.套接字发生错误,相应的读函数仍旧可能返回少于请求数目的数据。

readv/writev

1
2
3
4
5
6
7
8
#include <sys/io.h>
ssize_t readv(int flags, const struct iovec *iov, int iovcnt);
ssize_t writev(int flags, const struct iovec *iov, int iovcnt);

struct iovec {
void *iov_base; //缓冲区起始地址
void *iov_len; //缓冲区长度
};

注:

  • writev是原子调用,对于数据报协议,一次writev调用只会产生一个UDP数据报。
  • writev的一个用途:将小包数据经过整合,使用writev发送,可以有效防止Nagle算法的触发。

recvmsg/sendmsg

具体参见UNP14.6

四、获悉已排队的数据量

方法:

  • 如果获悉已排队数据量的目的在于不想阻塞,那么可以使用非阻塞调用。
  • 如果既想查看数据,有想保留数据在接收队列中以供本进程其他部分稍后读取,可以使用MSG_PEEK。通常需要结合非阻塞+PEEK,要么是非阻塞套接字+MSG_PEEK,或者阻塞+MSG_DONTWAIT+MSG_PEEK。对于TCP,先只“获悉”,再次“读取”,两次调用的返回数据长度可能是不同的;对于UDP,两次调用的返回数据是完全相同的。
  • ioctl + FIONREAD, 通过第三个参数(值——结果)返回当前接收队列的字节数。

C++11——理解右值

发表于 2018-05-17

概述

本文关注内容:右值和右值引用,以及移动语义、完美转发。

发现身边的右值

学习右值之所以会觉得很晦涩难懂,是因为我们完全习惯使用了左值。就像突然哪天告诉你,有“暗物质”的存在,你的“常识世界”是一下子不能接受的。

变量(或者说对象)可以看作是内存中的一小段。程序不断运行达到某种功能都是对若干个位于内存的对象,不断加工、修改的,各个对象又相互联系的结果。

在编程过程中,我们会定义各种变量:

1
2
3
4
5
6
7
int i = 0;

class ObjectA {
ObejctA() {};
};

ObjectA a();

这些定义操作,编译器会这样做:

  • 1.分配一段内存,命名为i,内存大小按照int类型标准分配,然后这段内存初始化为0;
  • 2.分配一段内存,命名为a,内存大小按照ObjectA类型标准分配,然后这段内存根据默认构造函数初始化。
    这样,在程序的其他地方,我们可以使用变量名i或者a,来达到修改它们所代表的内存的目的。比如:
    1
    i = 1;

这就是左值。它们是可以取地址的。它们会有个名字,就像门牌号。程序员可以明确地说:我就要使用某某地址上的那个变量i,我要把它改成1。

那么,内存世界里,除了程序员明确定义的变量占据内存,是否还存在其他构建了的内存对象?答案是有的。在程序编译期间和运行期间,会在内存构造很多没有名字的对象(匿名对象),它们有的为了计算,有的为了支持上层的语言特性。看以下几种情景:

1
int i = GetValue();

运行过程:程序通过GetValue()计算得出一个“结果”,然后将这个结果拷贝到i所命名的内存位置。我们考察这个“结果”(临时值),它同样需要一个和i一样的内存,只是在拷贝给i之后,这段内存就销毁了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ObjectA{
ObjectA(){
cout << "Constructor" << endl;
}

ObjectA(const ObjectA& ) {
cout << "Copy constructor" << endl;
}

~ObjectA() {
cout << "Destructor" << endl;
}
};

int main()
{
ObjectA a = ObjectA();
}

使用-fno-elide-constructors来取消返回值优化,以便观察临时值的产生。运行结果:

Constructor
Copy constructor
Destructor
Destructor

可以看出,这里构造了一个临时对象,然后拷贝给a之后,临时对象就销毁了。

此外,lambda表达式也属于右值,它们在内存中也会以某种对象的形式存在,但是你无法知悉它们的存储位置(获取地址)。

知道右值的存在,我们如何使用右值?

答案:右值引用。

和左值引用一样,右值引用允许你直接引用匿名对象的那段内存,虽然你无法知悉它的地址,但不影响你使用。编译器使用&&符号表示右值引用。

1
ObjectA &&a = ObjectA(); //语义:直接把临时构造的对象返回给a,a作为这个临时对象的引用供程序员使用。

这里运行结果:

Constructor
Destructor
1
2
auto f = [](){ return 1; }; //lambda表达式(匿名表达式)可以使用一个右值引用去“承接”它。
f(); //然后就可以使用这个右值引用,在其他地方调用这个lambda表达式

这里要区分好右值和右值引用。右值是一种对象的概念,而右值引用和左值引用一样,是引用。

使用右值,好处多多

通过前面的描述我们可以看到,很多时候,匿名对象(临时对象)的构造是不为人所知的。很多情况下,匿名对象的生命周期很短。它们被构造,然后在短时间内又被销毁。如果构造对象涉及开销比较大的操作,比如malloc。

我们来看一个配备移动构造函数的类对象的例子。

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
//没有移动构造函数
class ObjectA {
public:
ObjectA() : m_ptr(new char[1]) {
cout << "Constructor" << endl;
}

ObjectA(const ObjectA& a) : m_ptr(new char[*a.m_ptr]) //深拷贝
{
cout << "Copy Constructor" << endl;
}

~ObjectA() {
delete[] m_ptr;
cout << "Destructor" << endl;
}

private:
char* m_ptr;
};

int main()
{
ObjectA a = ObjectA(); //产生临时对象,该条语句运行完,临时对象销毁
}
Constructor
Copy Constructor
Destructor
Destructor

临时对象分配了动态内存,然后短时间内又析构释放。为了定义一个对象,总共发生了两次的new操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//配备移动构造函数
class ObjectA {
// ....

ObjectA(const ObjectA& a) : m_ptr(new char[*a.m_ptr])
{
cout << "Copy Constructor" << endl;
}

//移动构造函数
ObjectA(ObjectA && a) : m_ptr(a.m_ptr) { //
a.m_ptr = nullptr;
cout << "Move Constructor" << endl;
}
// ...
}
Constructor
Move Constructor
Destructor
Destructor

只发生一次new操作。我们将临时对象new的动态内存直接转移给了返回的对象。节省开销。

程序员明确知道,会产生临时变量,而且这个临时变量会在短时间内析构。那么何不将其捕捉,直接利用呢?移动构造函数就是利用这一点。

左值也可转化为右值——std::move()

左值是一种对象的概念,右值也是一种对象的概念。说白了,就是内存嘛。那么,一个左值可以用右值的方式看待,从而使用到右值的一些便利呢。答案是可以。

比如,我明确清楚某个对象我不会再使用了,但是里面的资源释放掉蛮可惜的,我希望新的对象或者其他对象直接来承接这些资源。std::move()模板函数提供了这样的功能。

1
2
3
4
5
6
int main()
{
ObjectA a = ObjectA();

ObjectA b(std::move(a)); //我们明确不会再使用a,就可以将其所只有的资源转移给新的对象。(具体转移操作由移动拷贝构造完成)
}

运行结果:
Constructor
Move Constructor
Destructor
move …
Move Constructor
Destructor
Destructor

类似具有转移语义的函数还有移动赋值语句

1
2
3
4
5
ObjectA& operator=(ObjectA&& a) {
cout << "operator= &&" << endl;
m_ptr = a.m_ptr;
a.m_ptr = nullptr;
}

完美转发 –std::forward模板

基本:一个&&的形参可以匹配左值,也可以匹配右值。&&称为universal reference。

需求:函数模板中,需要将参数转发给函数函数模板中调用的另一个函数。传入右值类型,转发的也要保留右值类型,传入左值类型,转发也要保留左值类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void process(int& i) { cout << "lvalue call, i = " << i << endl; }
void process(int&& i) { cout << "rvalue call, i = " << i << endl; }

template <typename T>
void func(T&& t)
{
process(t); // t是右值引用,但t本身是左值,所以匹配到的是process(int& i)
}

int main()
{
int a = 2;
func(a);

func(10);
}

运行结果:

lvalue call, i = 2
lvalue call, i = 10
1
2
3
4
5
template <typename T>
void func(T&& t)
{
process(std::forward<T>(t)); // t是右值引用,但t本身是左值,所以匹配到的是process(int& i)
}

运行结果:

lvalue call, i = 2
rvalue call, i = 10

使用完美转发和移动语义来实现一个泛型的工厂函数,这个工厂函数可以创建所有类型的对象

1
2
3
4
5
template <typename... Args>
T* Instance(Args... args)
{
return new T(std::forward<Args>(args)...);
}

[TODO]

  • std::move的实现
  • std::forward的实现

浅谈Select、Poll与Epoll的实现区别

发表于 2018-05-03 | 分类于 操作系统

本文关注内容:LinuxIO复用的三种机制,分别是Select、Poll以及Epoll。本文倾向于从浅谈三种机制的内核实现,来对比三种IO复用机制的性能差异。预备知识:等待队列和阻塞。

1 Select

1.1 Select API

1
2
#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds, fd_set* restrict writefds, fd_set* restrict exceptfds, struct timeval *restrict tvptr);

1.2 Select实现

  • 1.内核使用copy_from_user从用户空间拷贝fd_set到内核空间
  • 2.遍历所有关心的fd,如果fd就绪,就设置fd_set的值;如果fd不就绪,就将当前进程current加入到fd的等待队列(不休眠)。
  • 3.当遍历完所有的fd后,发现有fd就绪,则直接返回。如果没有一个是就绪的,那么调用schedule出让CPU进入睡眠(如果设置有超时时间,则调用schedule_timeout)。当有fd就绪的时候,会唤醒其等待队列上的进程。进程获得唤醒,再去重新遍历所有fd,判断是否有fd就绪。
  • 4.把fd_set从内核空间拷贝到用户空间。

1.3 Select的特点

  • 1.每次调用select都要重新设置fd_set。缺点:浪费时间,影响性能。
  • 2.调用select进入内核,或者从内核返回,都要拷贝一次fd集合。缺点:浪费时间,影响性能。
  • 3.文件描述符用fd集合组织,支持的数量大少,默认是1024。
  • 4.应用程序需要遍历整个fd_set,来获取哪些fd上的读写事件就绪。如果只有几个fd有就绪事件,遍历整个fd_set会造成极大的性能浪费。

2 Poll

2.1 Poll API

1
2
3
4
5
6
7
8
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd;
short events;
short revents;
};

2.2 Poll实现

内核实现基本和Select相同,只是有一点不同。从poll()函数的参数可以看出。文件描述符改用pollfd结构的数组组织,数量就比select多得多。

2.3 Poll特点

    1. 调用poll进入内核或者从内核返回,内核都要拷贝一次pollfd结构数组。缺点:浪费时间。
    1. 应用程序需要遍历整个pollfd结构数组,才能获取哪些fd有就绪事件。

3 Epoll

Epoll由Select、Poll发展而来。从Select、Poll的缺点来看,我们可以总结一下几个优化点:

  • 1.Select、Poll每次调用都是一次独立的注册、收集、返回的过程,很低效。–> Epoll将注册感兴趣事件和收集就绪事件分开。
  • 2.Select、Poll每次需要拷贝事件集合fd_set或事件数组到内核,收集事件时再拷贝到用户空间。 –> Epoll对同一个事件,只注册一次。
  • 3.处理就绪事件,Select、Poll需要遍历整个fd_set或这pollfd结构数组。 –> Epoll收集事件时只返回就绪的事件。

3.1 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/epoll.h> 

int epoll_create(int size); // 初始化一个epfd

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); //添加、修改、删除感兴趣事件

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); //一次事件收集

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events;
epoll_data_t data;
};

3.2 Epoll实现

epoll_create分配一个文件描述符以及对应的struct file结构。分配一个struct eventpoll结构用体于管理,在该结构体初始化红黑树头节点,初始化就绪链表,初始化等待队列等等。这个eventpoll结构是epoll的关键,它管理这一棵红黑树,一个就绪链表,一个等待队列等等。

  • 红黑树:高效的插入和删除操作。用于在添加、修改、删除感兴趣事件。
  • 就绪链表:存放已经就绪的事件。epoll_wait直接从这里拷贝事件返回用户。
  • 等待队列:如果有多个进程等待在这个epfd上,则会加入到这个等待队列。

epoll_ctl+ add操作:分配一个与要监听的fd对应的epitem,并初始化相应的成员,然后插入到红黑树。并将当前进程current挂到对应fd的等待队列,并注册一个回调函数。该回调函数会在fd就绪时,将事件结构体加入到就绪队列中(这点有设备驱动触发中断完成)。epoll_ctl的删除操作和修改差不多。

epoll_wait直接查看就绪链表是否为空,如果不为空,则拷贝事件到用户态,然后返回。如果为空,则加入epfd的等待队列睡眠,等待被唤醒(如果设置超时的话,则要么超时醒来,要么有事件就绪,醒来)。进程醒过来,对就绪链表逐一拷贝到用户态。这里是先拷贝中间链表,在拷贝期间发生的事件仍然能够加入到就绪链表,等待下次epoll_wait收集。

3.3 ET和LT的实现

其实内核在实现ET和LT的时候非常简单。就是在拷贝就绪链表的时候,内核判断事件类型,如果是ET模式,则直接清除掉。如果是LT模式,只要相应的fd仍是就绪的,则重新加入就绪链表。

另外有一个细节:在注册感兴趣的事件的时候,内核就会检查fd是否就绪,如果就绪,则直接加入就绪链表(可见,就绪事件加入就绪链表不一定是设备驱动触发的)。

3.4 Epoll的优点

  • 使用红黑树管理监听的文件描述符,高效的插入、删除操作。
  • 事件只需注册一次。
  • 支持监听的文件描述符的数量非常大。
  • 每次返回的是就绪的fd数组。Select、Poll需要遍历整个所有描述符。

本文完。

中断和异常——内核的动力

发表于 2018-03-01 | 分类于 操作系统

本文内容关注一种内核工作的基本机制——中断和异常。标题称这种机制为内核的动力,或许不那么恰当,主要想强调中断和异常对内核工作的重要性。对于操作系统上层的应用程序开发者而言,或许感觉离中断和异常的机制很远,因为我们不需要开发内核。但是毕竟我们使用的是内核所提供的各种服务,理解内核的基本运作原理能够帮助我们更好地写程序、优化程序。

问在前面:

  • 通常所说的“陷入内核”,是怎么个陷入法呢?
  • 我们的网卡收到数据,内核怎么知道该去处理数据了呢?时刻去轮询一下,恐怕不太现实?
  • 我们访问了一个非法的内存地址,或者执行了一个除0操作。毫无悬念,内核崩溃。那么内核是怎么知道进程崩溃了,该来收拾这个烂摊子了?

直观来看,内核是什么?

内核地址空间和进程的地址空间相互独立?

我们都知道,Linux内核与进程的地址空间相互独立。这是什么意思?直观的比喻就是,每个进程在自己的内存区域工作,内核也在自己的区域工作,它们之间互相都是不可见的。(这点由虚拟内存管理机制保证)。在同一个地址空间内部,我们可以简单的使用函数调用(或者说过程调用)来使用别的函数提供的功能。比如,你自己写的程序,函数funcA调用funcB。又比如,在Linux内核内部,函数可以直接调用。但是在两个地址空间之间,交互就没那么容易了。比如,如果进程想要使用内核提供的服务(文件读写,网络通信等等),怎么做呢?答案是,系统调用(陷入内核)。

系统调用怎么实现呢?

系统调用的目的就是进程能够使用内核的服务。但是具体是怎么向内核发起调用(请求)的呢。答案是中断。我们知道,CPU不断的取指令,执行指令。我们跟CPU约定好,如果遇到一种指令,叫“软中断指令”,CPU就保存当前进程的上下文,改变CPU的模式,然后跳转到内核的中断向量表,获取“软中断处理程序”的地址,然后执行。这就算陷入内核了。

内核是由中断驱动的一个程序实体

内核由各种各样的服务组件组成,各种系统调用、文件系统、网络系统、调度子系统等等。各种各样的内核活动的产生并不是凭空的,都是需要某种方式驱动,这就是中断。调度:定时器按一个时间片的周期触发一个中断,内核由此触发一次调度过程。系统调用:进程触发一个软中断,内核由此执行一个具体的系统服务。设备驱动:网卡收到数据,触发一次中断,内核由此执行一次对网卡的读操作。等等。

中断和异常

本质上,中断和异常属于同一个范畴。

我们知道,CPU的工作就是不断的取指令,执行指令。每执行一次指令,或者在执行指令期间会发生三种情况:

    1. 指令按预想的结果执行,然后取下一条指令,继续执行。
    1. 这是一条特殊指令,预示一种异常的流向。比如除0操作,越界访问,缺页异常或者系统调用的软中断指令。
    1. 执行期间,外部设备触发了一个中断,而CPU不得不响应这个中断,转去执行中断服务程序。这是另一种类型的异常流向。

对于第2种情况,我们称之为异常。就是CPU执行指令过程中出现的异常情况。因为它是CPU执行完一条指令之后触发的,所以也称为“同步中断”。对于第3种情况,称为“中断”,CPU随时都有可能收到外部设备的中断,具有随机性,也称之为“异步中断”。

不管是中断还是异常,它们的处理程序都是内核来执行的。

中断上下文与进程上下文

首先谈进程上下文:

示例:

同样是一次read调用,进程A和进程B得到效果是不一样的,进程A读取文件a,获取到x个字节;进程B读取文件b,获取到y个字节。而内核肯定只有一个read调用。
内核如何做到区别对待?

只告诉内核执行什么系统调用是不够的,内核必须知道,为哪个进程服务,读取哪个文件,预期读取多少个字节,要拷贝用户缓冲的目的地址等等。简言之,内核需要一个执行环境,这个环境就是所谓的“进程上下文”。即,当一个进程正在进行时,CPU所有寄存器中的值、进程的状态以及堆栈中的内容称之为进程的上下文。所以,进程执行系统调用,“内核代表进程执行,在进程上下文”。

再来就是中断上下文,和进程上下文是一个道理,当硬件触发中断的时候,内核需要获取到当前该硬件的各种变量和参数,内核根据这些参数来执行中断服务程序。而硬件传过来的各种参数和内核需要保存的一些环境(被中断的进程环境),可以看作是中断上下文。

总结

总的来说,内核就是一堆处理过程合成的实体。它基本没有主动性,靠的是各种中断来驱动的。进程有需求了,触发中断告诉它;程序执行出错了,触发中断告诉它;设备状态变化了,还是触发中断告诉它;时间片到期,该调度了,触发中断告诉它。

重新训练自己的大脑

发表于 2018-01-12 | 分类于 效率

阅读《学习之道》,重新训练自己的大脑。

What

  • 专注思维和发散思维
  • 工作记忆和长期记忆
  • 回想和提取练习
  • 组块构建和避免能力错觉

专注思维和发散思维

大脑存在两种思维模式交替运作:专注模式和发散模式。专注模式擅长在经验、旧有的神经连接中寻找解决方法;发散模式(无意识思考)则是在不经意间触发灵感。

沉于专注模式太久,会陷入思维困境无法自拔;没有专注一段时间便进入发散模式,往往陷入“认知错觉”(总感觉自己在学习,但实际上没有)。

学习新概念新知识块,首先需要快速浏览总体把握,并且尽可能描述清楚发现的问题。之后带着清晰的问题进入深度阅读,在深度阅读的过程中,不断明确问题,并尝试求解。在一段时间(1小时),问题仍旧不能解决,此时要转移注意力,有意识地切换成发散模式(散步,听音乐)。反复之。

专注模式下,问题必须清晰。

工作记忆和长期记忆

两种记忆系统:工作记忆和长期记忆

工作记忆是瞬时记忆,要想让工作记忆转存到长期记忆,需要时间也需要能量。也就是说,需要有时间表适时地不断排演重复,对其施加能量,才能触发生成长期的神经连接。一开始,最好24小时内回顾一次。之后每天重复一次,再后来,每周甚至几周重复一次。

这就意味着,需要有跟踪学习项目的记录。

回想,提取练习

回想(提取练习)是比一遍遍阅读材料更有效的,但是常常被忽略。

学习时,自我测验和做提取练习时最有效果的。

初次学到、还颇有挑战性的知识,最好是24小时内亲近一下。然后几天,一周,几周,几个月。

要想考出好成绩或在此基础上创造性思考,你就必须让它们牢牢地钉在记忆里。

组块构建和避免能力错觉

要熟练的掌握知识,就要创造一些概念组块——通过赋予意义将分散的信息碎片组合起来的过程。

构成组块的基本步骤

  • 注意力集中在原始信息本身的意义上
  • 理解信息形成组块的接口,即它对外界的意义
  • 获取背景信息,反复推敲组块和外界的连接,即其存在的意义和何时何地使用它们

搭建组块资料库的过程,也是训练大脑的过程。

仅仅看一遍问题答案,就以为自己会了,这就是能力错觉。从笔记里挑一个概念,看看自己能回忆起多少内容,同时试着理解所回忆的内容。此时,回想会发挥很大作用。

How

  • Questions List
  • ToDo List
  • 组块构建跟踪——笔记或Blog
  • 番茄时间(专注模式)

Q List

每开始一次新知识的学习,准备问题列表。在专注模式在,不断清晰、明确、深刻对问题本身和答案的理解。

组块构建跟踪

笔记本专门一白页(简要)、组块构建是Blog或笔记的形式

根据组块构建的步骤,一开始不应是总结式的。

  • 按大类别,将碎片化的知识小块,逐个理解(其本身,接口,与外界联系)
  • 心中已开始有全貌的认识,便开始整理、组块
  • 在尚未基本固定下来整个知识架构之前,按一定的时间周期,不断回想提取、理解。

ToDo List

每天睡觉前,写下明天要做的重要的事项。

避免拖延。

心态

  • 战胜恐惧,别怕落在别人后面
  • 拖延——令你没有足够时间处于专注模式,走马观花
  • 好好睡觉

static关键字

发表于 2017-12-05 | 分类于 编程语言

何时使用static

  • 请况一:当我们定义一个函数,该函数仅限于在定义它的源文件下使用,此时可以使用”static”修饰,以对外界隐藏该函数.
  • 情况二:同样,定义一个全局变量,该变量仅在定义它的源文件下使用,此时可以使用”static”修饰,达到隐藏的目的.
  • 情况三:对于一个局部变量,如果我们有意不让其存储在栈(自动变量)上,而是存储在静态存储区(.data,.bss),可以使用static修饰.
    前两种情况都是限定符号的作用域,第三种情况属于改变变量的生命周期.

static的作用如何实现的

首先清楚c/c++的分离式编译,每个.c文件都首先经过编译成目标文件(.o文件),再经过链接形成可执行文件。

static修改链接属性

使用static修饰全局变量或者函数,其实修饰的都是符号。对编译其来说,以static修饰的符号,其属性的本地属性,即在链接的时候,不会被链接器处理。这样就达到了向外界(其他目标文件)隐藏的目的。

static局部变量(静态局部变量)

静态局部变量仅仅是编译器在编译的时候,在程序的静态存储区(.bss,.data)为该变量预留空间。未初始化的静态局部变量存储在.bss段,已初始化的静态局部变量存储在.data。其生命周期就从初始化时刻到程序结束。栈上变量(自动变量)的生命周期只局限函数内部。

static和extern

extern并不能改变static的作用效果。

extern关键字的作用仅仅是告诉编译器,该符号的定义在外部。

  • 对于全局变量来说,extern的作用是告诉编译器,该符号在其他源文件里面。
  • 对于函数来说,extern的作用和#include “*.h”的作用是相似的,相当于声明。在两个目标模块的联系仅仅是几个函数,就可以使用这种方法,而不需要include包含整个头文件。
123
shunlqing

shunlqing

22 日志
7 分类
12 标签
E-Mail
© 2017 — 2019 shunlqing
本站访问量: 次 丨 本站访客: 人