面试题简答题——操作系统相关汇总

1.进程与线程

进程:进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;

线程:线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;线程是操作系统可识别的最小执行和调度单位。每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间(也就是同样的动态内存,映射文件,目标代码等等),打开的文件队列和其他内核资源;

区别:

1.一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程依赖于进程而存在;

2.进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量;)

3.进程是资源分配的最小单位,线程是CPU调度的最小单位;

4.系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销;一个进程的开销大概是一个线程开销的30倍左右;

5.通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预;

6.进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。

7.进程间不会相互影响 ;而一个线程崩溃将导致整个进程崩溃,从而影响进程内地其他线程;

8.进程适应于多核、多机;线程适用于多核;

多线程程序作为一种多任务、并发的工作方式,还有如下优点:

1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。

2.进程间通信方式

1、管道PIPE

1.1 无名管道:
1)它是半双工的,即单向通信,一端读一端写;
2)它只能用于父子进程or兄弟进程间的通信;
3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
4)遵循FIFO规则;

1.2 命名管道
1)解决无名管道只能用于近亲进程之间通信的缺陷而设计的;即可以在无关的进程间交换数据;
2)实现一个命名管道实际上就是实现一个FIFO文件;虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。

2、系统IPC(inter-Process Communication)

2.1 消息队列:

存放于内核;
面向记录,消息具有特定格式和优先级;
独立于发送接收进程,进程终止时,消息队列内容不会被删除;
可实现信息的随即查询,不遵守FIFO次序;

2.2 信号量机制

用于控制多个进程对共享资源的访问,实现进程间的同步和互斥,而不是用于存储进程间的通信;
信号量基于OS的PV操作,信号量的操作都具有原子性;

2.3 信号signal
一种比较复杂的通信方式,用于通知接受进程某个事件发生;

2.4 共享内存 (shared memory)
使多个进程可以访问同一块内存空间,需某种同步操作;

共享内存是最快的一种IPC,因为进程直接对内存进行存取;
信号量+共享内存结合使用,来同步对共享内存的访问;

3、套接字SOCKET
1、用于不同主机之间的进程通信;
2、用户认为的信息之间传输只是建立在两个应用程序上,实际上在TCP连接中是靠套接字来作为他们连接的桥梁;
TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点即为套接字 或 插口;
3、套接字用(IP地址:端口号)表示,区分不同应用程序进程间的网络通信和连接,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号;
在这里插入图片描述

3. 线程间的通信方式

1、临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
2、互斥量:保证公共资源不会被多个线程同时访问;
3、信号量;
4、事件(信号):通过通知操作的方式来保持多线程同步;

4. 操作系统中程序的内存结构

在这里插入图片描述
一个程序本质上都是由BSS段、data段、text段三个组成的;

BSS段(未初始化数据区):通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态分配,程序结束后静态变量资源由系统自动释放;可读可写 ;

data(数据段):存放程序中已初始化的全局变量的一块内存区域,也属于静态内存分配;可读可写;

text(代码段):存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读;在代码段中,也有可能包含一些只读的常数变量;

text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。

bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。

data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。

可执行程序在运行时又多出两个区域:栈区和堆区。

栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小;

堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多

5.fork 和 vfork

fork:若进程A执行fork()进程调用,则创建一个和进程A一样的进程映像(fork()用于创建新进程,而这个映像由exec()完成);

进程A成功调用fork()会创建一个新的进程A‘,A’几乎与A一模一样,且两个进程都会继续运行;fork()调用返回子进程的PID,表示成功;返回负数,则表示出现错误;

但调用fork时,内核把进程A所有的内部数据结构都复制一遍,其地址空间的内容逐页复制(Linux优化采用写时赋值)到A‘的地址空间中,比较耗时,开销大;

写时复制 copy_on_write:
基于懒惰算法;

写时复制的好处:
如果进程一直就不用修改资源,那么就不用复制,懒惰算法能够尽量推迟代价高昂的操作,直到必要时可才去执行;

vfork:为了降低fork后执行exec所造成的地址空间的浪费,引用vfork,vfork()的结果与fork()一样,但会挂起父进程直到子进程终止或者允许了一个新的可执行文件的映像;

意思就是,vfork()后,父进程和子进程共享相同的地址空间和页表项,子进程也无法修改地址空间的内存;即vfork()只将父进程内部的数据结构复制过去;

fork vfork
数据段方面 fork()子进程拷贝父进程数据段和代码段 vfork()子进程和父进程共享数据段
执行次序 父、子进程执行次序不确定 保证子进程先运行,直到调用exec或者exit后父进程可被调度运行,在这之前父、子进程数据段共享

6.并发(concurrency)和并行(parallelism)

并发
多个程序在同一时间间隔内完成,宏观上并行,微观上交叉执行;即在单个指令周期内只运行一个指令;并不能提高计算机性能,只能提高效率;

比如单核CPU上的多任务;

并行
严格物理意义上的同时运行,两个程序分别运行在两个核上,互不影响;单个指令周期内都运行了自己的指令,即两条指令;提高了计算机性能;

比如多核CPU;

7. 操作系统中的页表寻址

页式内存管理:
1、把内存空间划分为大小相等且固定的块,每个进程也以块为单位进行划分,进程在执行时,以块为单位逐个申请主存中的块空间;
2、进程中的块称为““,内存中的块称为“页框”,它俩大小相等;外存也以同样单位进行划分,直接称为”“;
3、进程在执行时需要申请主存空间,即为每个页面分配主存中的可用页框,这就产生了页框和页的一一对应;
4、操作系统为每一个进程维护了一个从页号到页框号的映射关系的数据结构,叫页表;页表中的每一项都记录了这个页号所对应的页框号;
5、通过页表,由逻辑地址的高位部分(页号)先找到其对应的页框号(内存中对应的物理块号),再由页框号加上逻辑地址的低位部分(偏移量)就得到最后的物理地址;
6、一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间;

8.单核机器上写多线程程序,是否需要考虑加锁?

在单核机器上写多线程程序,仍然需要线程锁;
因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突;

说明一下:
1、物理核数量=cpu数(机子上装的cpu的数量)*每个cpu的核心数
2、所谓的4核8线程,4核指的是物理核心;通过超线程技术,用一个物理核模拟两个虚拟核,每个核两个线程,总数为8线程;
在操作系统看来是8个核,但是实际上是4个物理核;
通过超线程技术可以实现单个物理核实现线程级别的并行计算,但是比不上性能两个物理核;
3、对于单核CPU和多核CPU,都是一个cpu,不同的是每个cpu上的核心数;
多核cpu是多个单核cpu的替代方案,多核cpu减小了体积,同时也减少了功耗;
一个核心只能同时执行一个线程;即单核CPU上的多线程也只是同一时刻只有一个线程在在执行,多线程同时执行是CPU快速地在多个线程之间的切换,cpu调度线程的时间足够快,就造成了多线程的“同时”执行;

引用文章:
->认识CPU、核和线程
->单核CPU如何执行超线程

9. 多线程与多进程;

多线程 多进程
描述 线程时CPU调度的最小单位 进程是资源分配的最小单位
优点 线程间通信简单,同步复杂,线程创建、销毁和切换简单,速度快,占用内存少 多进程间拥有各自独立的运行地址空间,进程间不会相互影响,同步简单,程序可靠性强
缺点 线程间会相互影响,一个线程意外终止会导致同一个进程的其他线程也终止,程序可靠性弱 进程创建、销毁和切换复杂,速度慢,占用内存多,进程通信复杂
适用 适用于多核分布式系统、适合I/O密集型的场景(因为I/O密集常由于I/O阻塞导致频繁切换现场,若是进程,则切换开销较大) 适用于多核、多机分布,适合CPU密集型场景

10.游戏服务器应该为每个用户开辟一个线程还是一个进程

游戏服务器应该为每个用户开辟一个进程。因为同一进程间的线程会相互影响,一个线程死掉会影响其他线程,从而导致进程崩溃。因此为了保证不同用户之间不会相互影响,应该为每个用户开辟一个进程;

11.死锁发生的条件以及解决死锁的策略

死锁是指两个或两个以上进程在执行过程中,因相互争夺对方所持有的资源而造成的相互等待的情况,此时若无外力干涉,这些进程都将得不到执行;

死锁发生的四个必要条件:
1、互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源;

2、请求和保持条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此时请求阻塞,但该进程不会释放自己已经占有的资源;

3、不可剥夺条件:进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放;

4、环路等待条件:进程发生死锁后,必然存在一个进程-资源之间的环形链;

解决死锁策略:

1、资源一次性分配,从而破坏请求和保持条件

2、可剥夺资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可剥夺的条件

3、资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件;

12.虚拟内存和物理内存怎么对应

在Linux下,逻辑地址与线性地址总是一致的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。

linux页式管理:

CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址。

线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。

另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
在这里插入图片描述
每个进程都有自己的页目录,当进程处于运行态的时候,其页目录地址存放在cr3寄存器中。

每一个32位的线性地址被划分为三部份,【页目录索引(10位):页表索引(10位):页内偏移(12位)】

依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);

2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。套娃,即页的地址被放到页表中去了。

3、根据线性地址的中间十位,在页表中找到页的起始地址;

4、将页的起始地址与线性地址中最后12位相加。

13.Linux的4种锁机制

1、互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该数据对象。当获取锁操作失败时,线程会进入阻塞,等待锁释放时被唤醒;

2、读写锁:rwlock,分为读锁和写锁。处于读操作时,可以允许多个线程同时获得读操作。但是同一时刻只能有一个线程可以获得写锁。其它获取写锁失败的线程都会进入阻塞状态,直到写锁释放时被唤醒;

注意:写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。适用于读取数据的频率远远大于写数据的频率的场合。

3、自旋锁:spinlock,在任何时刻同样只能有一个线程访问对象。但是当获取锁操作失败时,不会进入睡眠,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但如果加锁时间过长,则会非常浪费CPU资源。

4、RCU:即read-copy-update,在修改数据时,首先需要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据。使用RCU时,读者几乎不需要同步开销,既不需要获得锁,也不使用原子指令,不会导致锁竞争,因此就不用考虑死锁问题了。而对于写者的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其它写者的修改操作。在有大量读操作,少量写操作的情况下效率非常高。

14. 活动/静止 阻塞/就绪

1)活动阻塞:进程在内存,但是由于某种原因被阻塞了。

2)静止阻塞:进程在外存,同时被某种原因阻塞了。

3)活动就绪:进程在内存,处于就绪状态,只要给CPU和调度就可以直接运行。

4)静止就绪:进程在外存,处于就绪状态,只要调度到内存,给CPU和调度就可以运行。

15. A* a = new A; a->i = 10;执行的流程;

1)A *a:a是一个局部变量,类型为指针,故而操作系统在程序栈区开辟4 or 8字节的空间(0x000m),分配给指针a;

2)new A:通过new动态的在堆区申请类A大小的空间(0x000n);

3)a = new A:将指针a的内存区域填入堆中类A申请到的地址;即*(0x000m)=0x000n;0x000m代表指针a的地址;

4)a->i:先找到指针a的地址0x000m,通过a的值0x000n和 成员变量 i 在类 A 中的偏移量offset,得到a->i的地址0x000n + offset,进行*(0x000n + offset) = 10的赋值操作,即内存0x000n + offset的值是10;

15. 什么是大端小端以及如何判断大端小端

大端是指低字节存储在高地址;
小端是指低字节存储在低地址;
可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储;

int fun1(void)
    {
        union test
        {
            int a;
            char b;
        }t;
        t.a = 1;
        return (t.b==1);
    }

如果是大端 t.b=0;如果是小端 则t.b=1;
在这里插入图片描述

16.内存溢出和内存泄漏

1、内存溢出
指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误;

内存溢出原因:

内存中加载的数据量过于庞大,如一次从数据库取出过多数据;

集合类中有对对象的引用,使用完后未清空,使得不能回收;

代码中存在死循环或循环产生过多重复的对象实体;

启动参数内存值设定的过小;

2、内存泄漏

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

内存泄漏的分类:

1、堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。

2、系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

3、没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露;

17.协程

协程,又称微线程,纤程,英文名Coroutine。是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程;

协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)即在执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行;
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源;
不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多;

18. 僵尸进程

1、正常进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息,直到父进程通过wait / waitpid来取时才释放。保存信息包括:

1进程号

2退出状态

3运行时间等

2、孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

4、僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

僵尸进程是一个进程必然会经过的过程:这是每个子进程在结束时都要经过的阶段;

如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。

如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

危害
如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

外部消灭
通过kill发送SIGTERM或者SIGKILL信号消灭产生僵尸进程的进程,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源

内部解决:
1、子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。

2、fork两次,原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。

19. Linux进程状态

1、R 可执行状态
只有在该状态上的进程才可能在CPU上执行;

2、S 可中断的睡眠状态
处于这个状态的进程因为等待某事件的发生(比如等待socket连接、等待信号量),而被挂起;即阻塞态,当这一事件发生时可被唤醒;

3、D 不可中断的睡眠状态
进程处于睡眠状态,但是此刻进程是不可中断的;因为内核的某些处理流程是不能被打断的,如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了;
比如调用vfork()后,父进程进入此状态,直到子进程调用exit或exec;

4、T 暂停状态 or 跟踪状态
进程暂停下来,等待跟踪它的进程对它进行操作;向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态,除非该进程进入D状态,不响应信号;

5、Z 退出状态
此时,进程为僵尸进程,在 标题18处已描述;

20. GDB调试 以及 程序断点

GDB 是自由软件基金会(Free Software Foundation)的软件工具之一。它的作用是协助程序员找到代码中的错误;

如果没有GDB的帮助,程序员要想跟踪代码的执行流程,唯一的办法就是添加大量的语句来产生特定的输出。但这一手段本身就可能会引入新的错误,从而也就无法对那些导致程序崩溃的错误代码进行分析。

GDB的出现减轻了开发人员的负担,他们可以在程序运行的时候单步跟踪自己的代码,或者通过断点暂时中止程序的执行。此外,他们还能够随时察看变量和内存的当前状态,并监视关键的数据结构是如何影响代码运行的;

条件断点是当满足条件就中断程序运行,命令:break line-or-function if expr;

21.操作系统为什么区分用户态和内核态

为了安全性;在cpu的一些指令中,有的指令(即一些对系统影响较大的指令——特权指令)如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令时候,,可以通过系统调用陷入内核,让内核去执行这些操作;

22. 什么是线程池?为什么引入?如何实现?

线程池的引入:
1、创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率;
2、线程并发数量过多,抢占系统资源从而导致阻塞;
3,用户提交的任务能够及时的得到处理,提高响应速度;
4、对线程进行一些简单的管理;

线程池:线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中;

简单实现 :
1.设置一个生产者消费者队列,作为临界资源;
2.初始化n个线程,并让其运行起来,加锁去队列取任务运行;
3.当任务队列为空的时候,所有线程阻塞;
4.当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程;

23. 用户级线程和内核级线程的区别

用户级线程
在用户级线程中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在;即是一个多对一的模型,将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成;
缺点就是当一个线程在使用内核服务时,若被阻塞,则整个进程都会被阻塞;因为用户级线程对于内核级线程透明,对于后者来说,它只知道向用户提供了一个整体的接口;多个进程不能并行地运行在多处理机上;

内核级线程
线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程入口,即一对一模型;优点是当一个进程被阻塞后,允许另一个线程继续执行,所以并发能力较强;

缺点是每创建一个用户级线程都需要创建一个内核级线程与其对应,造成比较大的开销,影响性能;

24、多线程之间共享哪些资源?

一、同一进程间的线程共享内存地址空间,其中进程用户空间的五个段
英文: bss 、data 、 text 、 stack 、heap 。
中文:BSS段(bss segment),数据段:数据段(data segment),代码段:代码段(code segment/text segment),栈(stack)(栈又称堆栈), 堆(heap);

二、同一进程间的线程共享的资源有4个
a. 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的)

b. 全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的

c. 静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的

d. 文件等公用资源 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。

三、独享的资源 两个
a. 栈 栈是独享的,局部变量(非静态)位于栈中,自然也不共享;

b. 寄存器 这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC

线程共享的环境包括:(6个)
(1)进程代码段、(2)进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、(3)进程打开的文件描述符、(4)信号的处理器、(5)进程的当前目录和(6)进程用户ID与进程组ID。

【个性】进程拥有这许多共性的同时,还拥有自己的个性,才能实现并发性
1.线程ID
每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。

2.寄存器组的值
由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上 时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

3.线程的栈
栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数栈, 使得函数调用可以正常执行,不受其他线程的影响。

与线程“绑定”的是栈,用于存储自动变量。每一个线程建立的时候,都会新建一个默认栈与之配合。堆则是通常与进程相关,用于存储全局性的变量,进程建立的时候,会建立默认堆。于是,每一个线程都有自己的栈,然后访问共同的堆。

堆:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

4.错误返回码
由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该 线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。
5.线程的信号屏蔽码
由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都 共享同样的信号处理器。
6.线程的优先级
由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。

详情可参考:多线程究竟共享哪些资源

25、阿姆达尔定律

​ 阿姆达尔定律经常用于并行计算领域,用来预测适用多个处理器时理论上的最大加速比;

​ 在我们的性能调优领域,我们利用此定律有助于我们解决或者缓解性能瓶颈问题**。**

阿姆达尔定律的模型阐释了我们现实生产中串行资源争用时候的现象。如下图模型,一个系统中,不可避免有一些资源必须串行访问,这限制了我们的加速比,即使我们增加了并发数(横轴),但取得效果并不理想,难以获得线性扩展能力(图中直线)。

1

​ 在并行计算中,使用多个处理器的程序的加速比受限制于程序串行部分的执行时间。例如,如果一个程序使用一个CPU核执行需要20小时,其中部分代码只能串行,需要执行1个小时,其他19小时的代码执行可以并行,那么,不考虑有多少CPU可用来并行执行程序,最小执行时间不会小于1小时(串行工作的部分),因此加速比被限制为最多20倍(20/1)。

加速比越高,证明优化效果越明显

加速比=没有改进前的算法耗时/改进后的算法耗时

该定律要讨论的是为什么增加某些东西并不总能带来能力的翻番。该定律可应用在计算机行业,比如研究CPU的核数与性能的关系;在高性能计算领域,该定律可以解释为什么增加节点并不能带来性能的线性改善;

详解参考:阿姆达尔定律

猜你喜欢

转载自blog.csdn.net/wyll19980812/article/details/106007166
今日推荐