LRU算法读取淘汰双O(1)算法

 再次简介一下这个算法LRU全称是Least Recently Used,即最近最久未使用的意思。算法每一次将最久未使用的块更新出去。但是呢上一篇笔记附上的百度的代码复杂度实在不堪入目,所以该写还是得写,刚好也分享给大家这个百度前几页没有的O(1)算法。

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
#include<iostream>
#include<vector>

using namespace std;

const int cache_size = 10;
const int memory_size = 100;

struct cache_node
{
cache_node* l_link; // 栈的链接
cache_node* r_link;
int cache_id; // cache_id
int cache_val; // chache 映射位置
};

void init(cache_node*&head,cache_node*&tail)
{
head=new cache_node({nullptr,nullptr,0,-1});
cache_node *tmp=head;
for(int i(1); i<cache_size; i++)
{
cache_node *x = new cache_node({tmp,nullptr,0,-1});
tmp->r_link=x;
tmp = x;
}
tail = tmp;
}

int main()
{
vector<cache_node*>memory_to_cache(memory_size,nullptr);
cache_node *head,*tail;
init(head,tail);
int n;
cin>>n;
//n代表输入的数组组数,然后x代表这一次访问的对象,平均每次访问更新需要6次赋值,所以这个不是一个原子操作。假如cpu内部实现真的存在这种模式,内部大概会有许多门电路来使这多次赋值成为一个原子操作。
for(int i(0); i<n; i++)
{
int x;
cin>>x;
if(memory_to_cache[x]==nullptr)
{
cout<<tail->cache_val<<"被清出"<<endl;
cout<<x<<"移入栈顶"<<endl;
if(tail->cache_val!=-1)
memory_to_cache[tail->cache_val]=nullptr;
tail->l_link->r_link=nullptr;
tail->r_link=head;
head->l_link=tail;
tail=tail->l_link;
head=head->l_link;
head->l_link=nullptr;
head->cache_val=x;
memory_to_cache[x]=head;
}
else
{
if(head==memory_to_cache[x])
continue;
else
{
cout<<x<<"移入栈顶"<<endl;
memory_to_cache[x]->l_link->r_link=memory_to_cache[x]->r_link;
if(memory_to_cache[x]->r_link!=nullptr)
memory_to_cache[x]->r_link->l_link=memory_to_cache[x]->l_link;
else
{
tail=memory_to_cache[x]->l_link;
}
memory_to_cache[x]->l_link=nullptr;
memory_to_cache[x]->r_link=head;
head->l_link=memory_to_cache[x];
head=memory_to_cache[x];
}
}
}
return 0;
}

 完整的代码就是上面附的这一些了,然后由于代码比较简单主要是考察逻辑,所以也不再附更多的注释了。值得注意的是上面的代码的第一层if else中有数行功能相同,其实可以通过调整代码位置使其合并。但是过度合并可能会造成一些可读性缺失,这里就不再做深层次的代码优化了。

头条三面凉经分析

 非常惨淡的倒在了北京头条的三面上,首先觉得这个提前批的城市选择策略可能有误,太多人投递导致难度激增。第二个就是对操作系统算法的理解有天坑。这里需要弥补一下算法知识和一些深层的概念知识。

一 进程与线程在操作系统内核中的体现

 这个问题首先问的是进程和线程是什么样子的,由于前面已经对这个问题进行过总结,所以在此不再进行复述。

 然后说实话看到这个问题简直是惊呆了,课程里面确实没有详细学习过linux系统。基于操作系统调度的基本单位是进程,所以当时直接答出来线程应该在内核中没有体现。但是时候查阅资料发现这个问题回答的实在不规范。下面贴出标准答案的回复。

 首先需要肯定的是进程和线程在操作系统内核中确实概念相似。在linux系统中,进程和线程或者其他调度单元都是利用 task_struct 这个结构体表示。

1
2
3
4
5
6
7
struct task_struct {
...
pid_t pid; //标识不同的进程和线程
pid_t tgid; //用于标识线程组id,在同一进程中所有线程都具有同一tgid
...
struct *group_leader; //线程组中主线程的指针。
}

 进程还是线程的创建都是由父进程/父线程调用系统调用接口实现的。创建的主要工作实际上就是创建task_strcut结构体,并将该对象添加至工作队列中去。而线程和进程在创建时,通过CLONE_THREAD flag的不同,而选择不同的方式共享父进程/父线程的资源,从而造成在用户空间所看到的进程和线程的不同。

引用自Zpeg

 然后更加深入的去看待这个问题,无论以任何方式创建进程或者线程,在linux系统中最终都是去调用do_fork()函数。这个函数的原型是:

1
2
3
4
5
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
  • clone_flags是一个标志集合,用来指定控制复制过程的一些属性。最低字节指定了在子进程终止时被发给父进程的信号号码。其余的高位字节保存了各种常数。
  • stack_start是用户状态下栈的起始地址。
  • stack_size是用户状态下栈的大小。
  • arent_tidptr和child_tidptr是指向用户空间中地址的两个指针,分别指向父子进程的PID。NPTL(Native Posix Threads Library)库的线程实现需要这两个参数。

 线程跟进程的区别主要就在于do_fork函数首先执行的copy_process中,当上层以pthread_create接口call到kernel时,clone_flag是有CLONE_PTHREAD标识

但CLONE_PTHREAD标识只在最后一个步骤(设置各个ID、进程关系)时体现:(current为当前进程/线程的task_struct结构体 ,p为新创建的结构体对象)

1
2
3
4
5
6
7
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} else {
p->group_leader = p;
p->tgid = p->pid;
}
1
2
3
4
5
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);

 通过判断clone_flags中标记,即可控制子线程会与父线程共享虚拟地址空间、文件、信号等。

 本段的内容主要参考了Zpeg的博客,对博客中的部分内容进行精简,透过这里可以简要了解整体的流程。面对面试官的问题可以简要做如下回答:对于系统内核来说,进程和线程没有显著区别,它们在系统中都是一个task_struct结构体,他们的的主要差距是在主进程或者主线程进行系统调用的时候调用了do_fork()函数,然后do_fork函数中执行copy_process。在copy_process中有一个clone_flags,在创建线程的时候clone_flag是据有CLONE_PTHREAD标识的。

二 数据结构 b+树与跳表

 b+树的最大优势是可以利用叶子节点相互链接的特性来进行区间查询。这个特性无法被普通排序二叉树前序遍历使用链表链接替代。因为普通排序二叉树使用链表链接时更新下一跳的系统消耗可能过大。

 跳表在非尾部进行插入删除时更新索引开销较大。这里与排序二叉树链表链接时更新下一跳类似。

 关于这两个数据结构更多的内容不再进行展开描述。具体可以参考各大搜索引擎的介绍(推荐google)

三 深度学习lstm如何解决梯度爆炸

 这个问题我很吃惊,毕竟确实是本科生,制作深度学习模型的时候通常都是进行调库调试等操作。所以这里暂时不对这个模块进行扩展。来日方长,如果日后从事算法研究再战。

四 socket编程epoll模型的具体实现

 针对select模型和epoll模型,前段时间已经有过了解,select模型是通过轮询来获取活跃的连接的。但是仅知道epoll模型不会轮询所以效率高,但是并不了解epoll模型内部具体实现,这里对epoll模型内部实现机制进行一点介绍。

1
int epoll_create(int size);

 首先我们需要关注的是epoll的创建函数,参照晚风_清扬的博客对该函数进行下面的描述。

epoll_ create时,内核除了帮我们在epoll文件系统里建了新的文件结点,将该节点返回给用户。并在内核cache里建立一个红黑树用于存储以后epoll_ctl传来的需要监听文件fd外,这些fd会以红黑树节点的形式保存在内核cache里,以便支持快速的查找、插入、删除操作。

1
int int epoll_ctl(int epfd, intop, int fd, struct epoll_event *event);

内核还会再建立一个list链表,用于存储事件就绪的fd。内核将就绪事件会拷贝到传入参数的events中的用户空间。就绪队列的事件数组events需要自己创建,并作为参数传入这样才可以在epoll_wait返回时接收需要处理的描述符集合。

当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。

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

所以epoll模型的整体工作流程如下:

  1. 执行epoll_create时,创建了红黑树
  2. 执行epoll_ctl时,创建就绪list链表,如果增加fd添加到红黑树上,然后向内核注册有事件到来时的回调函数,当设备上的中断事件来临时这个回调函数会向list链表中插入就绪的fd。
  3. 执行epoll_wait时立刻返回准备就绪链表里的数据即可

 这里真的是吃了一个没有学过网络编程的亏,所有的内容几乎都是面试前突击的,但是通过这样重重面试对这些模型内部细节逐步有了一个清晰的认识。

五 linux copy-on-write特性

 其实很想跟面试官说,我真的真的真的没有系统的学习过linux操作系统,但是确实我学没学过跟你需要什么样的人才没有啥关系,只能吃一个哑巴亏回去赶紧补着点。

 copy-on-write 简单来说就是写时拷贝技术,该技术的主要目的时减少fork时复制主进程内存内容的时间开销。linux系统调用fork函数时,当进程未对内存内容进行修改的时候,此时将直接共享主进程的内存,此时子进程仅拥有对内存进行只读的权限。当需要对内存内容进行修改的时候,操作系统会先将内存copy一份,然后再进行写操作。

在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。

六 连续抛硬币问题

问题描述:

连续抛硬币,问出现正反反 和反反正的概率谁的更大概率是多少。

 面试的时候脑子有点糊,但是最后还是做出来了。分析过程大致如下:

 首先考虑下面四种情况:

1/4 NN

1/4 NF

1/4 FN

1/4 FF

 我们可以快速的发现,如果出现了正,后续只可能出现正反反胜利的情况。所以反反正胜利的概率仅存在于前两次抛出反的情况。然后对于情况反反,只要出现一次正,则反反正胜利,所以正反反胜利的概率则是 1-1/4 = 3/4。 反反正的概率是1/4。

七 LRU最近替换算法 O(1)

 看到这个问题实在是脑壳疼。操作系统课程上对这个内容学习的欠缺导致现在尴尬的局面。这个问题的解决非常非常的简单,利用一个映射数组和链表即可完成o(1)的更新和O(1)的访问。当然也可以利用hash_map和链表。个人推测cpu如果采用这种更新策略,应该使用的是映射数组的方式,这种方式更为稳定。这里需要 内存大小/页大小 的映射空间,在内存过大的时候可能并不是特别实用,所以推测现代cpu应该抛弃了这种方法,但是LRU算法仍然作为最经典的缓存算法被大家使用。

 因为心情郁闷这里就不对这个算法重新进行实现,摘选一个接近正确的代码放在下面:

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
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn=500005;
int n,m,x,cnt,stk[110],pos[maxn];
int main(){
while(cin>>n>>m){
cnt=0;
memset(pos,0,sizeof(pos));//标记页面在栈中的位置
while(m--){
cin>>x;
if(pos[x]){//栈中已出现x,将x提到栈顶,剩下的页面往下移
for(int i=pos[x];i<cnt;++i)stk[i]=stk[i+1],pos[stk[i]]=i;
//!!!这个代码不是特别对的地方就是这里,这里不能使用for循环,采用链表可以提升效率。
stk[cnt]=x,pos[x]=cnt;
}
else{//x未出现
if(cnt!=n)stk[++cnt]=x,pos[x]=cnt;//栈未满则直接添加
else{//栈满,移去最久未使用的页面,将x放栈顶
for(int i=1;i<n;++i)stk[i]=stk[i+1],pos[stk[i]]=i;
//!!!与上述同理。堆栈需要手动实现一个链表!!!!
stk[n]=x,pos[x]=n;
}
}
}
for(int i=1;i<=cnt;++i)cout<<stk[i]<<(i==cnt?'\n':' ');//输出当前栈中的页面号
}
return 0;
}

 然后关于这次心态崩坏的头条三面就总结到这里了,希望能够尽快顺顺利利的获得一个保研offer或者薪水客观的校招offer。加油!!!!!!

操作系统——io操作小结

 这里主要有四个概念,分别是同步io、异步io、阻塞io、非阻塞io。本文主要参照tengteng_的博客,简单总结这四个基本概念的含义。

一 同步

 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

 例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

二 异步

 异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

 例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

三 阻塞

 阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。

 有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占cpu去执行其他逻辑,也会主动检测io是否准备好。

四 非阻塞

 非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

再简单点理解就是:

  1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
  2. 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)。
  3. 阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
  4. 非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者

五 总结

同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞

阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回

 综上所述对此进行一些总结,通过学习生活中的观察我们可以发现,在我们日常生活中写的程序大多数都是同步的,通常来说我们的程序都需要立即执行得到结果。但是我们可以利用多线程或者进程设计一个异步的程序,在程序运行到一定的时候通过创建进程、线程来处理耗时较长的程序。而后在需要运算结果的时候对运算状态进行查询,这样可以使一个进程同时处理多种不同的任务。

本文引用自https://blog.csdn.net/crazy_tengt/article/details/79225913

操作系统——调度算法小结

 真的是太懵圈了,突然发现操作系统还有调度算法这么一说。虽说操作系统课程已经过去了一年多了但是这种基础知识着实不能忘。

一 先来先服务算法

 先来先服务算法是操作系统调度算法中最简单的算法,该算法可以用于作业调度,同样也可以用于进程调度,调度的策略非常简单,就是先来先服务。

二 短作业优先算法

 短作业有限算法,同样作为操作系统调度算法中非常简单的算法,调度策略是优先选取运行所需时间短的任务。

三 高权值优先算法

 这里分为抢占式和非抢占式算法,抢占式算法在高权值的任务进入时直接挤掉当前运行的任务。非抢占式算法在任务执行结束后,选取权值最高的任务执行。

四 高响应比优先算法

 由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。作业优先级随等待时间增高而增高。随着等待时间增加,将会越来越快的被分配到cpu

五 时间片轮转算法

 给队列中的每一个任务分配固定的时间片,然后每个任务执行完固定的时间片后将被轮出。但是这种算法如果时间片大小过大,则会造成cpu资源的浪费,如果时间片过小进程间不断切换也会浪费cpu资源。

六 多级反馈队列算法

 与时间片轮转算法非常相似,但是与其不同的式,拥有多个队列,当任务在第一个队列分配的时间片中无法完成全部任务时,将会被置入第二个队列中,分配给其的时间片大小会越来越大。由于大多数任务我们没有办法知道确切的运行时间。所以这种调度算法是目前最被广泛使用的调度算法。

操作系统基础

 本来以为自己的对于操作系统的基本理解应该是没有什么欠缺的,但是去搜刮了一波资料后发现自己好像啥都不会,本篇将主要总结:进程状态,CPU调度单位,中断实现机制,软硬中断的区别。

一 进程状态

 通常而言,进程的状态分为两大类,一种是三模态,一种是五模态,它们的简介分别如下:

一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器、有时虽有空闲处理器但因等待某个事件的发生而无法执行,这一切都说明进程和程序不相同,它是活动的且有状态变化的,这可以用一组状态加以刻画。为了便于管理进程,一般来说,按进程在执行过程中的不同情况至少要定义三种不同的进程状态:

  1. 运行(running)态:进程占有处理器正在运行。
  2. 就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行。
  3. 等待(wait)态:又称为阻塞(blocked)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。

通常,一个进程在创建后将处于就绪状态。每个进程在执行过程中,任意时刻当且仅当处于上述三种状态之一。同时,在一个进程执行过程中,它的状态将会发生改变。引起进程状态转换的具体原因如下:

  1. 运行态一一等待态:等待使用资源或某事件发生,如等待外设传输;等待人工干预。
  2. 等待态一一就绪态:资源得到满足或某事件己经发生,如外设传输结束;人工干预完成。
  3. 运行态一一就绪态:运行时间片到,或出现有更高优先权进程。
  4. 就绪态一一运行态:CPU空闲时被调度选中一个就绪进程执行。

 在实际的工作中,进程的状态转化通常比三模态更加复杂一些,所以这里就引入了一个五模态的概念。

在一个实际的系统里进程的状态及其转换比上节叙述的复杂一些,例如,引入专门的新建态(new)和终止态(exit )。

引入新建态和终止态对于进程管理来说是非常有用的。新建态对应于进程刚刚被创建的状态,创建‘个进程要通过两个步骤,首先,是为一个新进程创建必要的管理信息;然后,让该进程进入就绪态。此时进程将处于新建态,它并没有被提交执行,而是在等待操作系统完成创建进程的必要操作。必须指出的是,操作系统有时将根据系统性能或主存容量的限制推迟新建态进程的提交。

类似地,进程的终止也要通过两个步骤,首先,是等待操作系统进行善后;然后,退出主存。当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。引起进程状态转换的具体原因如下:

  1. NULL一一新建态:执行一个程序,创建一个子进程。
  2. 新建态一一就绪态:当操作系统完成了进程创建的必要操作,并且当前系统的性能和内存的容量均允许。
  3. 运行态一一终止态:当‘个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结。
  4. 终止态一一NULL:完成善后操作。
  5. 就绪态一一终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。
  6. 等待态一一终止态:未在状态转换图中显示,但某些操作系统允许父进程终结子进程。

五模态转换图解

 但是在linux系统中,进程的五种状态与上述的五模态还有一定的差别。linux系统中进程主要包含下面的五种状态:

  1. Linux进程状态:R (TASK_RUNNING),可执行状态&运行状态(在run_queue队列里的状态)
  2. Linux进程状态:S (TASK_INTERRUPTIBLE),可中断的睡眠状态, 可处理signal
  3. Linux进程状态:D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态, 可处理signal,有延迟
  4. Linux进程状态:T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态, 不可处理signal, 因为根本没有时间片运行代码
  5. Linux进程状态:Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程。不可被kill, 即不响应任务信号, 无法用SIGKILL杀死

 上述linux的五种状态在linux编程中有着非常重要的作用,这里很难三言两语将每个状态的作用详细介绍,日有有机会该部分将作为专题介绍。

二 CPU调度的最小单位

 我们知道,在操作系统中,进程是操作系统分配的最小单位,对于cpu调度的最小单位不同地方有不同的答案。许多人认为线程是执行流的最小单位,但是对于cpu调度来说,每次执行并不一定完成一个线程的全部任务。我们可以发现所有的cpu调度算法都是为进程分配时间片,所以这里可以认为,时间片是cpu调度的基本单位。

三 软中断、硬中断&&实现机制

 cpu和外部设备的速度总是不统一的,软件想要获取外部设备的状态主要有两种办法,一种是cpu不断地对所有的设备进行扫描,这样一旦设备发出信号,cpu就可以把时间告诉对应的应用程序。但是如果cpu同时连接了非常大量的外部设备,当一个设备发出信号时,cpu却在轮询其他设备,这中间的时间差会产生大量的延迟。而且cpu不断地访问空闲设备一定程序上也会浪费cpu的资源。

 所以在这里,操作系统会选择中断机制来解决这个问题。当没有中断产生的时候,cpu正常执行程序,当中断产生的时候,cpu会暂停当前进行的操作来处理中断,当中断处理完成后又将恢复执行正常程序。操作系统响应中断的基本流程如下。

产生中断——响应中断——关闭中断——保护中断——识别中断源——现场保护——中断服务——现场恢复。

 中断又常被分为软中断和硬中断对于他们的特点引用csdn博客内容:

硬中断:

  1. 硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。
  2. 处理中断的驱动是需要运行在CPU上的,因此,当中断产生的时候,CPU会中断当前正在运行的任务,来处理中断。在有多核心的系统上,一个中断通常只能中断一颗CPU(也有一种特殊的情况,就是在大型主机上是有硬件通道的,它可以在没有主CPU的支持下,可以同时处理多个中断。)。
  3. 硬中断可以直接中断CPU。它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。
  4. 对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程来运行。它的存在是为了让调度代码(或称为调度器)可以调度多任务。

软中断:

  1. 软中断的处理非常像硬中断。然而,它们仅仅是由当前正在运行的进程所产生的。
  2. 通常,软中断是一些对I/O的请求。这些请求会调用内核中可以调度I/O发生的程序。对于某些设备,I/O请求需要被立即处理,而磁盘I/O请求通常可以排队并且可以稍后处理。根据I/O模型的不同,进程或许会被挂起直到I/O完成,此时内核调度器就会选择另一个进程去运行。I/O可以在进程之间产生并且调度过程通常和磁盘I/O的方式是相同。
  3. 软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。一些内核允许设备驱动的一些部分存在于用户空间,并且当需要的时候内核也会调度这个进程去运行。
  4. 软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。有一个特殊的软中断是Yield调用,它的作用是请求内核调度器去查看是否有一些其他的进程可以运行。

 这里引用百度百科介绍中断的概念。

虽然人们在谈到中断(Interrupt)时,总会拿轮询(Polling)来做“反面”例子,但中断和轮询并不是完全对立的两个概念,呃,它们是对立统一的。

“CPU执行完每条指令时,都会去检查一个中断标志位”,这句话是所有关于中断长篇大论的开场白,但很容易被人忽略,其实,这就是中断的本质。

举个例子,CPU老板是一家公司的光杆司令,所有的顾客都要他亲自跑去处理,还要跟有关部门打点关系,CPU觉得顾客和公关这两样事它一个人搞不来,这就是轮询;终于这家公司升级发展了,CPU老板请了一个秘书,所有的顾客都先由秘书经手,CPU心情好的时候就去看一下,大部分时间都忙着去公关了,这时它觉得轻松了很多,这就是中断了~~

也就是说,中断和轮询是从CPU老板的角度来看的,不管怎样,事件都还是有人来时刻跟踪才能被捕获处理,不过是老板还是秘书的问题。所有的中断(或者异步,回调等)背后都有一个轮询(循环,listener)。

后端面试基础——进程与线程

 刚刚经历了大学以来的第一次面试,不得不说头条作为宇宙厂面试有非常高的难度,平日进行的简单开发和底层的深入理解不可而与,为了更加透彻的理解进程与线程,这里特别对进程和线程相关的知识进行总结。并且利用准确的术语来表述进程和线程的特点以及关系。

一 进程的基本概念

 进程是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。作为一个程序执行的一个实体,是系统分配资源的基本单位。

 进程通信主要有五种通信方式,分别是管道、命名管道、信号量、消息队列、共享内存。

二 线程的基本概念

 线程是进程的一个执行单元,是进程的一个特定执行路径。当一个线程修改了进程的资源,同一个进程下的其他线程将会立即看到这种变化。

 相较于进程,由于线程共享进程中的全局变量,所以通常而言不需要特别的线程通信方式,在C/C++中我们常利用volatile关键词使编译器不对代码段进行优化,无需将该值放入寄存器中,使该值可以被外部改变。或者利用windows消息队列进行线程通信。此外还可以利用std中std::promise、std::packaged_task来进行线程通信。

三 进程和线程的主要区别及优势

区别:

1.代码段、内存:进程间相互独立,同一进程的各线程间共享。线程仅在同一个进程内可见。

2.通信:进程间通信IPC(管道、命名管道、信号量、共享内存、消息队列),线程间通信相对进程间通信更为简单,直接使用全局变量即可。但是使用时需要考虑锁机制和同步机制。

3.调度和切换:线程上下文切换比进程上下文切换快得多。

4.在多线程OS中,进程不是一个可执行的实体。

选择:

1.需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程的代价是很大的。

2.线程的切换速度快,所以在需要大量计算,切换频繁时使用线程,还有耗时的操作时用使用线程可提高应用程序的响应。

3.因为对CPU系统的效率使用上线程更占优势,所以可能要发展到多机分布的用进程,多核分布用线程。

4.并行操作时用线程,如C/S架构的服务器端并发线程响应用户的请求。

5.需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

资源引用:什么是进程?什么是线程?进程和线程之间的区别是什么?

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×