多线程
并发
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。为了看起来像是“同时干多件事”,操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个应用使用。
操作系统时间片的使用是有规则的:某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做。此时CPU又分配给另一个作业去使用。
由于计算机的处理速度很快,只要时间片的间隔取得适当,那么一个用户作业从用完分配给它的一个时间片到获得下一个CPU时间片,中间有所”停顿”,但用户察觉不出来。
所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。
并行
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
这里面有一个很重要的点,那就是系统要有多个CPU才会出现并行。在有多个CPU的情况下,才会出现真正意义上的『同时进行』。
区别:
并发是指在一段时间内宏观上多个程序同时运行。并行指的是同一个时刻,多个任务确实真的在同时运行。
并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、
只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
线程池
把一堆线程弄到一起,统一管理,这种统一管理调度,循环利用线程的方式,就叫线程池(池式结构主要起到一个缓冲作用)
为了解决什么问题?
- 解决任务处理
- 阻塞IO
- 解决线程创建与销毁成本问题
- 管理线程
5. 异步解耦的作用
管理一个任务队列,一个线程队列,然后每次取一个任务分配给一个线程去做,循环往复。
任务队列、线程队列、管理组件。
实现方式:
在程序启动时,一次性创建好一定数量的线程。
线程池的拒绝策略
通常有以下四种策略:
丢弃任务并抛出异常。
丢弃任务,但是不抛出异常。
丢弃队列最前面的任务,然后重新提交被拒绝的任务
由调用线程(提交任务的线程)处理该任务
常见内存泄漏:
1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。比如,在一个Singleton类的构造函数中分配内存,在析构函数中却没有释放该内存。而Singleton类只存在一个实例,所以内存泄漏只会发生一次。
4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
如果一个程序存在内存泄漏并且它的内存使用量稳定增长,通常不会有很快的症状。每个物理系统都有一个较大的内存量,如果内存泄漏没有被中止(比如重启造成泄漏的程序)的话,它迟早会造成问题。
线程同步的方式
为什么要进行线程同步?
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
线程同步方式
(因为只用过thread,所以thread可以实现的就用thread语法来描述,不能实现的就用pthread)
一、互斥锁
一般会使用Mutex。Mutex就是一把锁,只有某些线程可以同时占用它(通过lock操作)。当线程不用的时候,就得通过unlock操作来释放它。
对于Mutex,std::thread和pthread差不多,无非是pthread_mutex_lock(&mutex)变成了mutex.lock()等等。
不过在std::thread中,mutex往往和lock系列模板一起使用。这是因为lock系列模板包装了mutex类,提供了RAII风格的加锁解锁。例如lock_guard和unique_lock,unique_lock比lock_guard灵活很多(多出来很多用法),效率差一点。
std::lock_guard<std::mutex> guard(mutex); // 加锁
...
// 自动解锁
unique_lock多了一个参数,unique_lock的第二个参数:
std::adopt_lock:表示这个互斥量已经被lock(),即不需要在构造函数中lock这个互斥量了。
std::try_to_lock:
尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;
使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方
前提:不能提前lock();
owns_locks()方法判断是否拿到锁,如拿到返回true
std::defer_lock:
如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex
二、条件变量
有时候线程之间需要某种同步——当某些条件不满足时,停止执行直到该条件被满足。这时候需要引入condition variable,状态变量。
std::condition_variable condvar;
consumer:
std::unique_lock<std::mutex> ulock(mutex);
condvar.wait(ulock, []{ return msgQueue.size() > 0;});
producer:
condvar.notify_all();
在一个线程调用wait之前,它必须持有unique_lock锁。当wait被调用时,该锁会被释放,线程会陷入沉睡,等待着生产者发过来的唤醒信号。当生产者调用同一个condition_variable的notify_all方法时,所有沉睡在该变量前的消费者会被唤醒,并尝试重新获取之前释放的unique_lock,继续执行下去。(注意这里发生了锁争用,只有一个消费者能够获得锁,其他消费者得等待该消费者释放锁)。如果只想叫醒一个线程,可以用notify_one。
通过wait_until和wait_for,你可以设定线程的等待时间。设置notify_all_at_thread_exit也许能帮得上忙。
条件变量通过运行线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足,常常和互斥锁一起使用,使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开响应的互斥锁并等待条件发生变化,一旦其他的某个线程改变了条件变量,它将通知响应的条件变量换线一个或多个正被此条件变量阻塞的线程,这些线程将重新锁定互斥锁并且重新测试条件是否满足
三、读写锁(pthread)
thread没有封装读写锁,只能用pthread库下面的
可以多个线程同时读,但是不能多个线程同时写
- 读写锁比互斥锁更加具有适用性和并行性
- 读写锁最适用于对数据结构的读操作读操作次数多余写操作次数的场合!
- 锁处于读模式时可以线程共享,而锁处于写模式时只能独占,所以读写锁又叫做共享-独占锁
- 读写锁有两种策略:强读同步和强写同步
在强读同步中,总是给读者更高的优先权,只要写者没有进行写操作,读者就可以获得访问权限
在强写同步中,总是给写者更高的优先权,读者只能等到所有正在等待或者执行的写者完成后才能进行读
int pthread_rwlock_rdlock(rwlock), 以读的方式获取锁
int pthread_rwlock_wrlock(rwlock),以写的方式获取锁
四、信号量
1)信号量初始化
int sem_init(&sem,pshared,v)
pshared为0表示这个信号量是当前进程的局部信号量
pshared为1表示这个信号量可以在多个进程之间共享
v为信号量的初始值,成功返回0,失败返回-1
2)信号量值的加减
int sem_wait(&sem):以原子操作的方式将信号量的值减去1
int sem_post(&sem):以原子操作的方式将信号量的值加上1
3)对信号量进行清理
int sem_destory(&sem)
补充:atomic,future
atomic位于头文件atomic下,实现了类似于java.util.concurrent.atomic的功能。它提供了一组轻量级的、作用在单个变量上的原子操作,是volatile的替代品。有些时候你也可以用它来替换掉Lock(假如整个race condition中只有单个变量)
future包装了未来某个计算结果的期诺。当你对所获得的future调用get时,程序会一直阻塞直到future的值被计算出来。(如果future的值已经计算出来了,get调用会立刻获得返回值)而这一切都是在后台执行的。
#include <chrono>
#include <iostream>
#include <future>
using namespace std;
int main()
{
future<int> f1 = async(launch::async, [](){
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
return 0;
});
future<int> f2 = async(launch::async, [](){
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
return 1;
});
cout << "Results are: "
<< f1.get() << " " << f2.get() << "\n";
return 0;
}
死锁
什么是死锁
在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
死锁产生的条件
互斥条件:进程对于所分配到的资源具有排它性,即资源不能被共享,只能由一个进程使用(一个资源只能被一个进程占用,直到被该进程释放 。)
请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
非剥夺条件:任何一个资源在没被该进程释放之前,任何其进程都无法对他剥夺占用。
循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。
递归死锁:在多线程的环境下使用递归,遇到了多线程那么就不得不面对同步的问题。而递归程序遇到同步的时候很容易出问题。
多线程的递归就是指递归链中的某个方法由另外一个线程来操作。
死锁的情况
(1)忘记释放锁
(2)单线程重复申请锁
(3)双线程多锁申请
(4)环形锁申请
死锁的解决方法
1.避免嵌套锁定
2.在已经持有锁的时候不要调用用户自义的代码
3.按固定顺序锁定
4.用层锁来防止死锁
5. 尽量减少资源占用的时间,可以降低死锁的发生的概率
6. 银行家算法。
内核态和用户态
内核空间和用户空间
针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。(对于64位linux系统,一般可用虚拟地址位数为48位,其中,0x0000000000000000~ 0x00007fffffffffff 表示用户空间, 0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF 表示内核空间)。每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。
为什么需要区分内核空间与用户空间
操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。
在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。
内核态和用户态
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。
如何从用户空间进入内核空间
其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。可以通过内核提供的接口来完成这样的任务。
读取文件示例:
向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”(通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间))
在内核空间中,CPU 从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。
用户态进入到内核态有三种方式:系统调用、软中断和硬件中断。
从内核空间和用户空间的角度看一看整个 Linux 系统的结构
在硬件之上,内核空间中的代码控制了硬件资源的使用权,用户空间中的代码只有通过内核暴露的系统调用接口(System Call Interface)才能使用到系统中的硬件资源。
虚拟地址到物理地址的映射
从上面我们知道,一个程序编译连接后形成的地址空间是一个虚拟地址空间,但是程序最终还是要运行在物理内存中。因此,应用程序所给出的任何虚地址最终必须被转化为物理地址,所以,虚拟地址空间必须被映射到物理内存空间中,这个映射关系需要通过硬件体系结构所规定的数据结构来建立。这就是我们所说的段描述符表和 页表,Linux主要通过页表来进行映射。
虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始的,如图4.2所示,之所以这么规定,是为了在内核空间与物理内存之间建立简单的线性映射关系。其中,3GB(0xC0000000)就是物理地址与虚拟地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
操作系统中的缺页中断
malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
1、保护CPU现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU现场,继续执行
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
1、在指令执行期间产生和处理缺页中断信号
2、一条指令在执行期间,可能产生多次缺页中断
3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。
linux操作系统
Linux 常见的进程调度算法
进程的状态:
1).运行态:该状态表明进程在实际占用CPU
2).就绪态: 该状态下进程可以运行,但因为其他进程正在运行而暂时停止
3).阻塞态: 该状态下进程不能运行,除非某种外部事件的发送
运行态→阻塞态
往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
阻塞态→就绪态
则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态
不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
就绪态→运行态
系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态
进程调度的方式
非剥夺方式
分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程完成或发生某事件而阻塞时,才把处理机分配给另一个进程。
剥夺方式
当一个进程正在运行时,系统可以基于某种原则,剥夺已分配给它的处理机,将之分配给其它进程。剥夺原则有:优先权原则、短进程优先原则、时间片原则。
关于调度即是如果只有一个CPU可用,那么就必须选择下一个要运行的进程,在操作系统中,完成选择工作的这一部分称为调度程序,该程序使用的算法称为进程算法。进度调度就是按照一定的策略,动态地把处理机分配给处于就绪队列的进程,使之执行。
常见的进程调度算法:
1.先来先服务和短作业(进程)优先调度算法
1)先来先服务调度算法
2)短作业(进程)优先调度算法
2.高优先权优先调度算法
优先权调度算法的类型
1) 非抢占式优先权算法
2) 抢占式优先权调度算法
3) 高响应比优先调度算法
3.基于时间片的轮转调度算法
1)时间片轮转法
2)多级反馈队列调度算法
虚拟内存的好处
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
虚拟内存的好处:
1.扩大地址空间;
2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
4.当进程通信时,可采用虚存共享的方式实现。
5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片
虚拟内存的代价:
1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
2.虚拟地址到物理地址的转换,增加了指令的执行时间。
3.页面的换入换出需要磁盘I/O,这是很耗时的
4.如果一页中只有一部分数据,会浪费内存。
操作系统中的页表寻址
页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。
Linux最初的两级页表机制
两级分页机制将32位的虚拟空间分成三段,低十二位表示页内偏移,高20分成两段分别表示两级页表的偏移。
- PGD(Page Global Directory): 最高10位,全局页目录表索引
- PTE(Page Table Entry):中间10位,页表入口索引
当在进行地址转换时,结合在CR3寄存器中存放的页目录(page directory, PGD)的这一页的物理地址,再加上从虚拟地址中抽出高10位叫做页目录表项(内核也称这为pgd)的部分作为偏移, 即定位到可以描述该地址的pgd;从该pgd中可以获取可以描述该地址的页表的物理地址,再加上从虚拟地址中抽取中间10位作为偏移, 即定位到可以描述该地址的pte;在这个pte中即可获取该地址对应的页的物理地址, 加上从虚拟地址中抽取的最后12位,即形成该页的页内偏移, 即可最终完成从虚拟地址到物理地址的转换。从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做page talbe walk。
Linux的三级页表机制
Linux的四级页表机制
Linux的4种锁机制:
互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒
读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入睡眠状态,直到写锁释放时被唤醒。 注意:写锁会阻塞其它读写锁。当有一个线程获得写锁在写时,读锁也不能被其它线程获取;写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。
自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。
RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。
进程的五种基本状态
1)创建状态:进程正在被创建
2)就绪状态:进程被加入到就绪队列中等待CPU调度运行
3)执行状态:进程正在被运行
4)等待阻塞状态:进程因为某种原因,比如等待I/O,等待设备,而暂时不能运行。
5)终止状态:进程运行完毕
线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的
线程在切换的过程中需要保存当前线程Id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下
SP:堆栈指针,指向当前栈的栈顶地址
PC:程序计数器,存储下一条将要执行的指令
EAX:累加寄存器,用于加法乘法的缺省寄存器
chmod的使用,741表示什么
chmod作用:
修改文件、目录的权限
Linux/Unix 的文件调用权限分为三级 : 文件拥有者、群组、其他。利用 chmod 可以藉以控制文件如何被他人所调用。
u 表示该文件的拥有者,g 表示与该文件的拥有者属于同一个群体(group)者,o 表示其他以外的人,a 表示这三者皆是。
+ 表示增加权限、- 表示取消权限、= 表示唯一设定权限。
r 表示可读取,w 表示可写入,x 表示可执行,X 表示只有当该文件是个子目录或者该文件已经被设定过为可执行。
此外chmod也可以用数字来表示权限
chmod abc file
其中a,b,c各为一个数字,分别表示User、Group、及Other的权限。
r=4,w=2,x=1
若要rwx属性则4+2+1=7;
若要rw-属性则4+2=6;
若要r-x属性则4+1=5。
chmod 741 file 表示用户的权限是rwx,群组的权限是可读,其它人的权限是可执行
请问如何修改文件最大句柄数?
linux默认最大文件句柄数是1024个,在linux服务器文件并发量比较大的情况下,系统会报"too many open files"的错误。故在linux服务器高并发调优时,往往需要预先调优Linux参数,修改Linux最大文件句柄数。
- ulimit -n <可以同时打开的文件数>,将当前进程的最大句柄数修改为指定的参数
- 对所有进程都有效的方法,修改Linux系统参数
vi /etc/security/limits.conf 添加
* soft nofile 65536
* hard nofile 65536
将最大句柄数改为65536
OS缺页置换算法
当访问一个内存中不存在的页,并且内存已满,则需要从内存中调出一个页或将数据送至磁盘对换区,替换一个页,这种现象叫做缺页置换。当前操作系统最常采用的缺页置换算法如下:
先进先出(FIFO)算法:置换最先调入内存的页面,即置换在内存中驻留时间最久的页面。按照进入内存的先后次序排列成队列,从队尾进入,从队首删除。
LFU算法
**LFU(Least Frequently Used ,最近最少使用算法)**也是一种常见的缓存算法。
顾名思义,LFU算法的思想是:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
LFU 算法的描述:
设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:
set(key,value):将记录(key,value)插入该结构。当缓存满时,将访问频率最低的数据置换掉。get(key):返回key对应的value值。
算法实现策略:考虑到 LFU 会淘汰访问频率最小的数据,我们需要一种合适的方法按大小顺序维护数据访问的频率。LFU 算法本质上可以看做是一个 top K 问题(K = 1),即选出频率最小的元素,因此我们很容易想到可以用二项堆来选择频率最小的元素,这样的实现比较高效。最终实现策略为小顶堆+哈希表。
LRU算法
LRU(The Least Recently Used,最近最久未使用算法)是一种常见的缓存算法,在很多分布式缓存系统(如Redis, Memcached)中都有广泛使用。
LRU算法的思想是:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。
LRU算法的描述: 设计一种缓存结构,该结构在构造时确定大小,假设大小为 K,并有两个功能:
set(key,value):将记录(key,value)插入该结构。当缓存满时,将最久未使用的数据置换掉。get(key):返回key对应的value值。
实现:最朴素的思想就是用数组+时间戳的方式,不过这样做效率较低。因此,我们可以用双向链表(LinkedList)+哈希表(HashMap)实现(链表用来表示位置,哈希表用来存储和查找),在Java里有对应的数据结构LinkedHashMap。
A* a = new A; a->i = 10;在内核中的内存分配上发生了什么?
1)A *a:a是一个局部变量,类型为指针,故而操作系统在程序栈区开辟4/8字节的空间(0x000m),分配给指针a。
2)new A:通过new动态的在堆区申请类A大小的空间(0x000n)。
3)a = new A:将指针a的内存区域填入栈中类A申请到的地址的地址。即*(0x000m)=0x000n。
4)a->i:先找到指针a的地址0x000m,通过a的值0x000n和i在类a中偏移offset,得到a->i的地址0x000n + offset,进行*(0x000n + offset) = 10的赋值操作,即内存0x000n + offset的值是10。
静态变量什么时候初始化
静态变量存储在虚拟地址空间的数据段和bss段,C语言中其在代码执行之前初始化,属于编译期初始化。而C++中由于引入对象,对象生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造。
实时和分时操作系统
什么是实时操作系统
实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的操作系统。其特点是及时响应和高可靠性。实时系统又分为硬实时系统和软实时系统,硬实时系统要求在规定的时间内必须完成操作,这是在操作系统设计时保证的;软实时则只要按照任务的优先级,尽可能快地完成操作即可。
什么是分时操作系统?
使一台计算机同时为几个、几十个甚至几百个用户服务的一种操作系统。把计算机与许多终端用户连接起来,分时操作系统将系统处理机时间与内存空间按一定的时间间隔,轮流地切换给各终端用户的程序使用(时间片的概念)。由于时间间隔很短,每个用户的感觉就像他独占计算机一样。
实时操作系统需要满足哪些特征
**多任务:**由于真实世界的事件的异步性,能够运行许多并发进程或任务是很重要的。多任务提供了一个较好的对真实世界的匹配,因为它允许对应于许多外部事件的多线程执行。系统内核分配CPU给这些任务来获得并发性。
抢占调度:真实世界的事件具有继承的优先级,在分配CPU的时候要注意到这些优先级。基于优先级的抢占调度,任务都被指定了优先级,在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。换句话说,当一个高优先级的任务变为可执行态,它会立即抢占当前正在运行的较低优先级的任务。
任务间的通讯与同步: 在一个实时系统中,可能有许多任务作为一个应用的一部分执行。系统必须提供这些任务间的快速且功能强大的通信机制。内核也要提供为了有效地共享不可抢占的资源或临界区所需的同步机制。
任务与中断之间的通信: 尽管真实世界的事件通常作为中断方式到来,但为了提供有效的排队、优先化和减少中断延时,我们通常希望在任务级处理相应的工作。所以需要在任务级和中断级之间存在通信。
分时操作系统需要满足哪些特征
交互性: 用户与系统进行人机对话。
多路性: 多用户同时在各自终端上使用同一CPU。
独立性: 用户可彼此独立操作,互不干扰,互不混淆。
及时性: 用户在短时间内可得到系统的及时回答。