操作系统
- 操作系统
- 进程间通信
- 线程间通信
- 进程和线程的关系和区别
- 什么是内存泄漏?
- Linux虚拟地址空间
- 操作系统中的程序的内存结构
- 堆与栈的区别
- 操作系统中的缺页中断
- Linux修改最大文件句柄数
- 虚拟内存的置换方式?
- 多进程运行环境的内存超载情况处理?
- 用户态和内核态区别
- 死锁产生的原因及四个必要条件
- 系统调用是什么,有哪些系统调用
- 孤儿进程、僵尸进程
- 单核机器上写多线程程序,是否需要考虑加锁,为什么?
- 线程需要保存哪些上下文,SP、PC、EAX这些寄存器什么作用
- 游戏服务器应该为每个用户开辟一个线程还是一个进程,为什么?
- 操作系统中的结构体对齐,字节对齐
- A* a = new A; a->i = 10;在内核中的内存分配上发生了什么?
- 请问什么是大端小端以及如何判断大端小端
- 静态链接和动态链接
- 协程
- 硬链接和软链接
进程间通信
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存)、以及套接字socket。
- 管道pipe: 管道是一种半双工的通信方式,数据只能单向流动,而且只能在
具有亲缘关系
的进程间使用。进程的亲缘关系通常是指父子进程关系。 - 命名管道FIFO: 有名管道也是半双工的通信方式,但是它
允许无亲缘关系进程间的通信
。 - 消息队列MessageQueue: 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享存储SharedMemory: 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
共享内存是最快的 IPC 方式
,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信
。 - 信号量Semaphore: 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 套接字Socket: 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
扩展:
信号量的本质
信号量在本质上是一种数据操作锁(计数器,记录统计临界资源的数目)
。它本身不具备数据交换的功能,而是通过保护其他的通信(文件、外部设备)等临界资源来实现进程间通信。信号量在此过程中负责数据操作的互斥、同步等功能。
信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和 V(sv),sv为信号量,它们的行为如下:
P(sv): 如果sv的值大于0,就对其减1;如果它的值为0,就挂起该进程的执行。
V(sv): 如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有进程因等待sv而被挂起,就给它加1。
进程间通信——信号量(Semaphore) (opens new window)
线程间通信
Linux系统中的线程间通信方式主要以下几种:
锁机制: 包括互斥锁、条件变量、读写锁
- 互斥锁提供了以排他方式防止数据结构被并发修改的方法。
- 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
- 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制(Semaphore): 包括无名线程信号量和命名线程信号量
- 允许多个线程同时访问同一个资源,但是会限制同一时刻访问此资源的最大线程数目。
信号机制(Signal): 类似进程间的信号处理
- 通过通知操作的方式保持多线程同步,还可以方便的实现多线程优先级的比较操作。
线程间的通信目的主要是用于线程同步
,所以线程没有像进程通信中的用于数据交换的通信机制。
进程和线程的关系和区别
- 概念: 进程是资源申请、调度和独立运行的单位。线程是进程的一个单一的连续控制流程,是CPU调度的最小单位。
- 数据共享: 进程数据是分开的: 共享复杂,需要用IPC,同步简单;多线程共享进程数据: 共享简单,同步复杂(线程所私有的: 线程id、寄存器的值、栈、线程的优先级和调度策略、线程的私有数据、信号屏蔽字、errno变量、)
- 上下文切换: 进程创建销毁、切换复杂,速度慢 ;线程创建销毁、切换简单,速度快;
- 资源占用: 进程占用内存多, CPU利用率低;线程占用内存少, CPU利用率高;
- 通信: 进程间不会相互影响 ;进程的一个线程挂掉将导致整个进程挂掉
什么是内存泄漏?
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
对于C和C++这样的没有Garbage Collection 的语言来讲,我们主要关注两种类型的内存泄漏:
- 堆内存泄漏(Heap leak)。堆内存指的是程序执行中依据须要分配通过malloc,realloc new等从堆中分配的一块内存,再是完毕后必须通过调用相应的 free或者delete 删掉。假设程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
- 系统资源泄露(Resource Leak).主要指程序使用系统分配的资源,比如 Bitmap,handle ,SOCKET等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
- 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
解决方法:
- 使用智能指针;
Linux虚拟地址空间
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上
。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常
。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
用户进程部分分段存储内容如下表所示(按地址递减顺序):
名称 | 存储内容 |
---|---|
栈(stack) | 局部变量、函数参数、返回地址等 |
内存映射段(mmap) | 内核将硬盘文件的内容直接映射到内存;用于映射可执行文件用到的动态链接库 |
堆(heap) | 用于存放进程运行时动态分配的内存段,可动态扩张或缩减。 |
BSS段(Block Started by Symbol) | 未初始化或初值为0的全局变量和静态局部变量 |
数据段(data) | 用于存放程序中已初始化且初值不为0的全局变量和静态局部变量 |
代码段(text) | 可执行代码、字符串字面值、只读变量 |
每个区域是依靠着两个指针进行维护的,比如[start_data,end_data)是用来维护data段,[start_code,end_data)用来维护code段,[start_brk,brk),用来维护heap和heap的指针。[start_stack,end_stack)是用来维护stack段空间范围。mmap_base是维护共享映射区的起始地址。bss段表示的是所有的未初始化的全局变量,为了效率,对处在bss段的变量,将它们匿名映射到“零页”,这样提高了程序的加载效率。
PCB通过mm_struct这个结构体里面的数据描述,来组织管理进程的地址空间
虚拟内存的好处:
扩大地址空间;
内存保护: 每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
当进程通信时,可采用虚存共享的方式实现。
当不同的进程使用同样的代码时,比如库文件中的代码,
物理内存中可以只存储一份这样的代码
,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片。
虚拟内存的代价:
虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
虚拟地址到物理地址的转换,增加了指令的执行时间。
页面的换入换出需要磁盘I/O,这是很耗时的
如果一页中只有一部分数据,会浪费内存。
操作系统中的程序的内存结构
一个程序本质上都是由BSS段、data段、text段三个组成的。可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
BSS段(未初始化数据区)(Block Started by Symbolsegment):
- 通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。BSS段属于静态分配,程序结束后静态变量资源由系统自动释放。
数据段:
- 存放程序中已初始化的全局变量的一块内存区域。数据段也属于静态内存分配
代码段:
- 存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域属于只读。在代码段中,也有可能包含一些只读的常数变量
text段和data段在
编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的
。- bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
- data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。
- 数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。
可执行程序在运行时又多出两个区域:栈区和堆区。
- 栈区:由编译器自动释放,存放函数的参数值、局部变量等。
每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间
。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。 - 堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
- 栈区:由编译器自动释放,存放函数的参数值、局部变量等。
堆与栈的区别
堆与栈实际上是操作系统对进程占用的内存空间的两种管理方式,主要有如下几种区别:
管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小 64bits 的 Windows 默认 1MB,64bits 的 Linux 默认 10MB;
生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
存放内容不同。
栈存放的内容,函数返回地址、相关参数、局部变量和寄存器内容等。
当主函数调用另外一个函数的时候,`要对当前函数执行断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,即扩展指针寄存器的内容(EIP),然后是当前栈帧的底部地址,即扩展基址指针寄存器内容(EBP),再然后是被调函数的实参等,一般情况下是按照从右向左的顺序入栈,之后是被调函数的局部变量,注意静态变量是存放在数据段或者BSS段,是不入栈的。出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来填充的。
从以上可以看到,堆和栈相比,由于大量malloc()/free()或new/delete的使用,容易造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈相比于堆,在程序中应用较为广泛,最常见的是函数的调用过程由栈来实现
,函数返回地址、EBP、实参和局部变量都采用栈的方式存放。虽然栈有众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,主要还是用堆。
无论是堆还是栈,在内存使用时都要防缺页止非法越界,越界导致的非法内存访问可能会摧毁程序的堆、栈数据,轻则导致程序运行处于不确定状态,获取不到预期结果,重则导致程序异常崩溃,这些都是我们编程时与内存打交道时应该注意的问题。
操作系统中的缺页中断
malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。
缺页中断: 在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存是,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
1、保护CPU现场
2、分析中断原因
3、转入缺页中断处理程序进行处理
4、恢复CPU现场,继续执行
但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:
1、在指令执行期间产生和处理缺页中断信号
2、一条指令在执行期间,可能产生多次缺页中断
3、缺页中断返回是,执行产生中断的一条指令,而一般的中断返回是,执行下一条指令。
Linux修改最大文件句柄数
为了提升服务器并发访问能力,有时我们需要修改最大支持打开文件的数量,使用 ulimit 工具将很容易实现这点。
- 首先,查看当前能够打开文件的数量
- ulimit -a 可以看到,open files 为1024, 而且提示了参数为 -n,下面我们修改该值
- 修改最大文件句柄数
- ulimit -n 65536
虚拟内存的置换方式?
虚拟内存是计算机系统内存管理的一种技术。 它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
虚拟地址空间: 在C语言中我们称之为程序地址空间,而Linux下称之为进程虚拟地址空间.
页面置换算法产生的原因: 由于虚拟地址空间中,他本身并不具备存储数据的能力,只是将部分数据存储在外存中,当使用该数据的时候,才将他加载到内存中;
常见的页面置换算法:
- 最优置换算法(OPT)
- 先进先出算法(FIFO)
- 最近最少使用替换算法(LRU)
- 最不经常访问淘汰算法(LFU)
多进程运行环境的内存超载情况处理?
当多个进程竞争内存资源时,会导致内存资源紧张,并且,如果此时没有就绪进程,处理机会空闲,I/O速度比处理机速度慢得多,可能出现全部进程阻塞等待I/O。
针对以上问题,提出两种解决方案:
交换技术,换出一部分进程到外存,腾出内存空间。
虚拟内存,每个进程在只有一部分程序和数据被调入内存的情况下运行。
用户态和内核态区别
用户态和核心态的区别(线程锁时会提及)
- 操作系统需要两种CPU状态:
内核态(Kernel Mode): 运行操作系统程序
用户态(User Mode): 运行用户程序
- 指令划分:
特权指令: 只能由操作系统使用、用户程序不能使用的指令。 举例: 启动I/O 内存清零 修改程序状态字 设置时钟 允许/禁止终端 停机
非特权指令: 用户程序可以使用的指令。 举例: 控制转移 算数运算 取数指令 访管指令(使用户程序从用户态陷入内核态)
- 特权级别:
特权环: R0、R1、R2和R3
R0相当于内核态,R3相当于用户态;
不同级别能够运行不同的指令集合;
- CPU状态之间的转换:
用户态--->内核态: 唯一途径是通过中断、异常、陷入机制(访管指令)
内核态--->用户态: 设置程序状态字PSW
- 内核态与用户态的区别:
1)内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;
2)当程序运行在0级特权级上时,就可以称之为运行在内核态。
3)运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们
在系统中执行一个程序时,大部分时间是运行在用户态下
的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。4)这两种状态的主要差别是: 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ; 而处于内核态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
通常来说,以下三种情况会导致用户态到内核态的切换:
1)系统调用
- 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。比如前例中fork()实际上就是执行了一个创建新进程的系统调用。
- 而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
2)异常
- 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
3)外围设备的中断
- 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
死锁产生的原因及四个必要条件
产生死锁的原因主要是:
- 因为系统资源不足。
- 进程运行推进的顺序不合适。
- 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则 就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
产生死锁的四个必要条件:
- 互斥条件: 一个资源每次只能被一个进程使用。
- 请求与保持条件: 一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
系统调用是什么,有哪些系统调用
在计算机中,系统调用指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供了用户程序与操作系统之间的接口,即系统调用是用户程序和内核交互的接口。
Linux下常见的系统调用:
- 进程控制: fork 创建一个新进程 , getpid 获取进程标识号, wait 等待子进程终止
- 文件读写操作: fcntl 文件控制, open 打开文件, creat 创建新文件, close 关闭文件描述字, read 读文件, write 写文件
- 文件系统操作: chdir 改变当前工作目录, chmod 改变文件方式, rmdir 删除目录, rename 文件改名
- 系统控制: reboot 重新启动, time 取得系统时间
- 内存管理: mmap 映射虚拟内存页, getpagesize 获取页面大小
- Socket 套接字 :socket 建立socket, bind 绑定socket到端口, connect 连接远程主机, accept 响应socket连接请求
- 用户管理、消息、管道、信号量、共享内存
孤儿进程、僵尸进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
僵尸进程: 一个进程使用fork创建子进程,如果子进程退出终止,而父进程尚未调用wait或waitpid获取子进程的状态信息,那么子进程的进出描述符仍然保持在系统中,变成僵尸(Zombie)进程。(特别注意,僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。)
- 僵尸进程是一个进程必然会经历的过程: 这是每个进程在结束时都需要经过的阶段。
- 如果子进程在exit()之后,父进程没来得及处理,ps查看子进程状态为Z;
- 如果父进程能及时处理,ps命令来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。
- 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
利用命令ps,可以看到有父进程ID为1的进程是孤儿进程;s(state)状态为Z的是僵尸进程。
危害:
如果进程不调用wait/waitpid的话,保留的那段信息就不会释放,进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果产生大量的僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
僵尸进程的清除:
外部消灭: 通过kill发送SIGTERM或者SIGKILL信号消灭产生僵尸进程的进程,它产生的僵尸进程就变成了孤儿进程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源。
子进程退出时向父进程发送SIGCHLD信号,父进程处理此信号,在信号处理函数中调用wait()处理僵尸进程。
fork()两次来清除僵尸进程。原理是将子进程变为孤儿进程,从而使其父进程变为init进程,通过init进程来处理僵尸进程。父进程fork()一个子进程,然后继续工作,子进程再fork()一个子进程,即孙进程后退出,那么孙进程就将被init接管,孙进程结束后,init会回收。不过,子进程的回收还是要自己处理。
单核机器上写多线程程序,是否需要考虑加锁,为什么?
在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
线程需要保存哪些上下文,SP、PC、EAX这些寄存器什么作用
线程在切换的过程中需要保存当前线程Id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下:
- SP:堆栈指针,指向当前栈的栈顶地址
- PC:程序计数器,存储下一条将要执行的指令
- EAX:累加寄存器,用于加法乘法的缺省寄存器
游戏服务器应该为每个用户开辟一个线程还是一个进程,为什么?
游戏服务器应该为每个用户开辟一个进程。因为同一进程间的线程会相互影响,一个线程死掉会影响其他线程,从而导致进程崩溃。因此为了保证不同用户之间不会相互影响,应该为每个用户开辟一个进程。
操作系统中的结构体对齐,字节对齐
- 原因:
- 1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
2、规则 * 1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。 * 2)结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。 * 3)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
3、定义结构体对齐
可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。
4、举例
#pragma pack(2)
struct AA {
int a; //长度4 > 2 按2对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 2 按1对齐;偏移量为4;存放位置区间[4]
short c; //长度2 = 2 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 2 按1对齐;偏移量为7;存放位置区间[8];共九个字节
};
#pragma pack()
A* a = new A; a->i = 10;在内核中的内存分配上发生了什么?
- A *a:a是一个局部变量,类型为指针,故而操作系统在程序栈区开辟4或8字节的空间(0x000m)(32位4字节,64位8字节),分配给指针a。
- new A:通过new动态的在堆区申请类A大小的空间(0x000n)。
- a = new A:将指针a的内存区域填入栈中类A申请到的地址的地址。即*(0x000m)=0x000n。
- a->i:先找到指针a的地址0x000m,通过a的值0x000n和i在类a中偏移offset,得到a->i的地址0x000n + offset,进行*(0x000n + offset) = 10的赋值操作,即内存0x000n + offset的值是10。
请问什么是大端小端以及如何判断大端小端
大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储。
静态链接和动态链接
- 静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
- 动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。