最后一遍复习:带你通过字节跳动面试(1)

  1. 管道通信
  • 管道通信分为普通管道和命名管道。普通管道可用于有亲缘关系进程之间的通信,命名管道还允许无亲缘关系进程之间的通信。
  • 管道上数据是单方向传输的,想要完成双向通信需要两个管道。
  1. 消息
  • 将通信的数据封装在消息中,通过消息来完成通信。消息的通信方式有两种:
  1. 直接通信方式,将消息直接发送给对方进程。

  2. 间接通信方式,双方都通过共享中间实体来完成对消息的法松和接收。

  3. 信号量

  • 信号量本质上是一个计数器,用来完成进程的互斥和同步,比如  操作。
  1. 共享内存
  • 不同进程可以访问同一块内存,共享内存是临界资源。共享内存直接从内存中读取数据,不需要从用户态到内核态的切换,是最快的一种方式。

线程之间的同步方式

  1. 临界区:多线程访问公共资源,速度快。
  2. 互斥量:控制多个进程对他们之间共享资源的互斥访问。由于资源只有一个,所以不能被多个线程同时访问。
  3. 信号量:允许多个线程访问同一资源,但同一时刻访问该资源的线程有最大的数目限制。

线程之间哪些是共享的

  1. 堆区,堆是进程开辟出来的,多线程共享这部分资源。
  2. 全局变量和静态变量,和特定线程无关,所以也是共享的。
  3. 文件等公共资源,使用这些公共资源的线程必须同步。

线程需要保存哪些上下文

线程在切换过程中,需要保存当前线程 、线程状态、堆栈、寄存器状态等信息。寄存器状态主要包括:

  1. :堆栈指针,指向当前栈的栈顶指针。
  2. :程序计数器,存储下一跳将要执行的指令。
  3. :累加寄存器,用于加法乘法的缺省寄存器。

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

进程。因为同一进程内的线程会相互影响,所以如果一个用户的线程死掉了,其他用户的游戏也会崩溃。所以应该为每个用户开辟一个进程,使用户之间不会相互影响。

多进程和多线程的使用场景

多线程模型适用于  密集型场合。因为经常会因为  阻塞来切换线程,而线程切换的系统开销比进程切换小。

多进程模型适用于需要频繁计算的场合。

多线程是不是越多越好

并不是

  1. 因为多线程意味着需要更多的内存资源。
  2. 并不是同时运行多个线程,而是轮流执行了,如果线程过多,  就要在不同的线程之间快速切换,那么  的利用率就会降低。

++ 线程中锁机制

  • 互斥锁。互斥锁用于控制多个线程对他们之间共享资源的互斥访问的一个信号量。避免了多个对象同时访问一个共享资源。此时它会被放到等待队列里,然后去处理其他任务。
  • 条件锁。当共享数据达到某个值时,唤醒正在等待这个数据的线程,若没有共享数据分配时,向申请的线程挂起。
  • 自旋锁。当一个线程想要请求一个资源,但这个资源被别的线程占用时,它会一直处于请求状态而不能去做其他事情(忙等),所以自旋锁容易造成死锁。只有内核是可抢占式的且当临界区资源很快可以被分配到的时候,才考虑采用自旋锁。
  • 读写锁。分为读锁和写锁。允许多个线程同时进行读操作,但同一时间只允许一个线程进行写操作。

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

仍然需要线程锁。

线程锁通常用来实现线程的同步和通信,在单核机器上仍然存在线程同步的问题。在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽以后,操作系统会把它挂起,然后运行另一个线程。如果两个线程共享某些数据,但没有线程锁,可能会导致同享数据修改引起冲突。

死锁发生的条件以及如何解决死锁

死锁是指两个或者两个以上进程在执行过程中,因为争夺资源而造成相互等待的现象。

四个发生条件分别为:

  1. 互斥条件。进程分配到的资源不允许其他进程同时访问。若其他进程想要访问该资源,只能等待到占用资源的进程使用完。
  2. 请求和保持条件。进程获得一定资源后,又对其他资源发出请求。如果其他资源被占用,此时请求阻塞,但该进程不会释放以有的资源。
  3. 不可剥夺条件。进程获得的资源在使用完成之前不可被剥夺,只能自己使用完后释放。
  4. 循环等待条件。发生死锁时,一定存在一个 进程——资源 的循环链。

解决死锁的方法:

  1. 破坏请求和保持条件。在进程开始运行之前,必须一次性分配该进程需要的所有资源。
  2. 破坏不可剥夺条件。当一个进程对新资源的请求又不能被满足时,必须释放已经获得的所有资源。
  3. 破坏循环等待条件。系统对所有资源类型进行编号,每个进程必须按序号递增的顺序请求资源。如果想要请求序号较低的资源,必须释放已经获得的高序号的资源。

虚拟内存技术

传统存储器存在问题:当有的作业很大或同一时间有大量作业要求运行时,其需要的内存空间超过了内存总容量,作业无法全部装入,导致作业无法运行。这都是由于传统存储器要求一次性装入作业导致的,所以采用了虚拟内存。

虚拟内存技术使进程在运行过程中,内存中只装入了当前要运行的少数页面,其余部分暂存在外存上。如果程序访问的页尚未调入内存中,便发出缺页中断, 将需要的页调入内存。如果内存满了,无法装入新的页时,便会使用页面置换方式将暂时不用的页调至外存,再将要访问的页调入内存。

虚拟内存的优点:

  1. 可以更加高效的使用物理内存。
  2. 使内存的管理更加便捷。在编译程序的时候使用虚拟地址,就不会因为物理地址有时被占用而需要重新编译了。
  3. 更加安全。每个进程运行在各自的虚拟内存地址空间中,互相不干扰对方。

虚拟内存的缺点:

  1. 虚拟内存需要建立额外的数据结构,需要占用额外的内存。
  2. 虚拟地址到物理地址的转换增大的运行时间。
  3. 页面的换入换出需要磁盘 ,需要耗费很大的时间。

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

请求分页存储管理中一般使用二级页表。

  1. 从  中取出页表地址。
  2. 根据地址前十位,找到对应的索引项。此时获得页目录的地址,而不是页的地址。
  3. 根据地址中间十位,从页表中获得该页的起始地址。
  4. 将获得的页的起始地址和最后  位地址相加,获得想要的物理地址。

操作系统中的缺页中断

通过  分配内存时,只是分配了虚拟内存而不是实际的物理地址,进程访问时也是访问的虚拟地址而不是物理地址。

在请求分页系统中,可以查询页表的状态来确定要访问的页表是否在内存中。每当要访问的页面不存在内存中时,就会发生一个缺页中断,然后操作系统会将缺失的页调入到内存中。

缺页中断的处理一般分为  个步骤:

  1. 保护  现场
  2. 分析中断原因
  3. 转入缺页中断处理程序进行处理
  4. 恢复  现场并继续处理

虚拟内存置换的方式

当访问一个不存在内存中的页时,需要从外存调入。如果此时内存已满,就需要调出一个页到外存,在将需要的页调入。这个过程叫做缺页置换。

  1. 最佳置换算法:调出的页面是未来不访问或最久不访问的页面,但由于实际过程中无法预知未来,这是一种理论的算法。
  2. 先进先出  页面置换算法:置换掉最早调入内存的页面,也就是说在内存中按队列的形式管理页,从队尾插入,从队首删除。
  3. 最近最久未使用  置换算法:置换掉最近一段时间内最久未访问的页面。根据局部性原理,刚刚被访问过的页面可能马上又要被访问,而较长时间未访问的页面可能最近不会访问。
  4. 最少使用  置换算法。置换掉最近一段时间访问次数最少的页面。
  5. 置换算法。为每一页设置一个访问位,再将页面设置成循环队列。在选择一个页面时,如果访问位是 ,就把它置换掉,如果是 ,就把访问位置为  并开始检查下一个页面。

为什么要有

是  的高速缓存,可以加快读取数据的速度。

在页式存储结构中,需要先访问内存获得数据物理地址,然后再去物理地址中读出数据。但访问内存的速度比较慢,所有引入了 ,访问一个地址时,先去  中查找,如果命中了就可以直接获得其物理地址,然后访问数据,这样可以大大增加访问速度。

种  模型

  1. 阻塞 。一直检查  事件是否就绪,没有就继续等待,期间什么事也不做。
  2. 非阻塞 。每隔一段时间检查一下  事件是否就绪,没有就绪就做其他事。
  3. 信号驱动 。安装一个信号处理函数,进程继续运行。当  事件就绪时,进程会收到  信号,然后处理  事件。
  4. 多路复用。 多路复用在阻塞  上多了个  函数, 函数可以看后面。
  5. 异步 。应用程序把  请求给内核后,由内核去完成相关操作。当内核完成相关操作后,会发信号告诉应用进程本次  已经完成。

水平触发和边缘触发

  1. 水平触发(状态达到):当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写。如果用户一次读写没取完数据,他会一直通知用户。如果这个描述符是用户不关心的,它每次都返回通知用户,则会导致用户对于关心的描述符的处理效率降低。
  2. 边缘触发(状态变化):当被监控的文件描述符上有可读写事件发生时,会通知用户程序去读写,它只会通知用户进程一次,这需要用户一次把内容读取完,相对于水平触发,效率更高。如果用户一次没有读完数据,再次请求时,不会立即返回,需要等待下一次的新的数据到来时才会返回,这次返回的内容包括上次未取完的数据。

和  是水平触发的。 支持水平触发也支持边缘触发,但默认是水平触发的。

消息机制

当用户使用键盘或者鼠标时,系统会把这些操作转化成消息。系统会将这些消息放入消息队列中,然后对应的进程会循环从消息队列中取出消息,完成对应的操作。

僵尸进程

  1. 正常进程
  • 正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。但子进程的结束和父进程的运行是一个异步过程,也就是说父进程无法预知子进程什么时候结束。一个进程完成他的工作后,它的父进程要调用  函数来收集子进程的终止状态,并把他彻底销毁后返回,如果没有等到这样的一个子进程,就会阻塞在这里等待。
  1. 孤儿进程
  • 如果一个父进程推出,而他的子进程还在运行,那么这些子进程就变成了孤儿进程。孤儿进程会由  进程收养。
  1. 僵尸进程
  • 如果子进程退出,而父进程没有使用  函数,那么这些进程的进程描述符仍然保存在系统中,这些进程被称为僵尸进程。
  • 僵尸进程是一个必经的阶段,如果子进程退出,父进程还没有使用  函数,此时就是僵尸进程,等到父进程处理以后才消失。如果父进程在子进程结束之前退出,子进程就会由  进程接管, 进程会以父进程的身份处理僵尸进程。

僵尸进程的危害:

  • 僵尸进程如果不被释放,就会一直占用系统的进程号。而系统的进程号是有限的,如果有大量的僵尸进程,可用的进程就会减少。

如何避免僵尸进程

  • 外部解决

  • 通过  消灭产生僵尸进程的进程,那么僵尸进程就变成了孤儿进程,由  进程处理。

  • 内部解决

  • 子进程退出时向父进程发送信号,父进程接收到信号时,在信号处理中调用  处理僵尸进程

  • 两次 :父进程  后马上 ,子进程在  一次后马上 ,孙进程完成父进程中本来要完成的事情,由于是孙进程的父进程已经退出了,它变成了孤儿进程,由  进程处理。

线程池

线程池就是首先创建一些线程,它们的集合称为线程池。线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。线程池可以很好的提高性能。

线程池工作流程

  1. 初始化线程池、任务队列和工作线程
  2. 向任务队列中添加任务
  3. 将等候在条件变量(任务队列上有任务)上的一个线程唤醒并从该任务队列中取出第一个任务给该线程执行
  4. 等待任务队列中所有任务执行完毕
  5. 关闭线程池

如果线程的空闲时间过长,就可以考虑缩小线程池。

大端小端以及如何判断大端小端

  • 大端:指低字节存储在高地址
  • 小端:指低字节存储在低地址

可用通过  来判断系统是大端还是小端,因为  总是从低地址开始存放

int main() {
// freopen(“in”, “r”, stdin);
union T {
int x;
char y;
} t;
t.x = 1;
if(t.y == 1) printf(“xiao duan\n”);
else printf(“da duan\n”);
return 0;
}

fork、vfork、select、poll、epoll函数

  • 函数通过系统调用创建一个和原来进程几乎一模一样的进程,两个进程可以做同一件事,如果传的参数不同也可以做不同的事。在子进程中,成功的  会返回 ,在父进程中  会返回子进程的 ,失败会返回负数。

  • 的调用和作用和  是一致的。但存在一些区别:

  1. 的子进程拷贝父进程的地址空间, 的子进程和父进程共享地址空间。
  2. 的子进程和父进程执行顺序不定, 保证子进程先执行,父进程在执行。
  • 函数是实现  多路复用的一种方式。 函数监听程序的文件描述符集,由数组来描述哪个文件描述符被置位了。当某个文件描述符就绪时,就会返回所有的描述符集,然后应用程序去检查哪个文件描述符上有事件发生。 函数还存在一些缺点:

  • 内置数组的形式使最大文件数受限

  • 每次调用前,都要把文件描述符集从用户态拷贝到内核态,每次调用后,都要从内核态拷贝到用户态

  • 轮询排查的方式在文件描述符多时效率很低

  • 函数通过一个可变长度的数组解决了  函数中文件描述符受限的问题。

  • 函数把要监听的描述符添加进去,这些描述符会组成一颗红黑树。当某个描述符上有事件发生时,会把对应的文件描述符添加到链表中,然后返回链表。 相较与  的优点在于:

  • 支持监听大数目的文件描述符。 最大为 , 可以远远大于这个值。

  • 效率上提高。 返回时不可以把有事件的描述符筛选出来,需要在遍历一遍,而  返回时会加到一个链表中,然后直接对链表操作。

后父子进程的内存关系

  1. 首先可以确定的是,代码是相同的,所以父子进程会共用代码段
  2. 对于数据部分,一开始时,子进程的页表项指向和父进程相同的物理内存页。而当父进程或子进程想要对这些页面做修改之前,操作系统会拷贝要修改的页面,并对父子进程的页表项做出相应的调整。

进程内存空间

从高地址的到低地址分别为:

  • 内核空间

  • :负责管理进程的所有资源

  • 用户空间

  • 栈:由编译器自动分配释放,存放函数的参数值、局部变量等。

  • 堆:用户通过  动态分配释放。

  • 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。

  • 数据段:存放程序中已初始化的全局变量的一块内存区域。

  • 代码段:存放代码的一块内存区域,同时还会存储一些常数变量。该段是 只读 的。

多核  进程调度算法

  • 全局队列调度

  • 操作系统维护一个全局的任务等待队列。

  • 当系统中有一个  核心空闲时,操作系统就从全局任务等待队列中选取就绪任务开始在此核心上执行。

  • 这种方法的优点是  核心利用率较高。

  • 局部队列调度。

  • 操作系统为每个  内核维护一个局部的任务等待队列。

  • 当系统中有一个  内核空闲时,便从该核心的任务等待队列中选取恰当的任务执行。

  • 这种方法的优点是任务基本上无需在多个  核心间切换,有利于提高  核心局部  命中率。

  • 目前多数多核  操作系统采用的是基于全局队列的任务调度算法。

磁盘的物理结构

通过 (柱面号、盘面号、扇区号) 的三元组来定位到要读数据的位置。

最后

Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。

人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。

资源持续更新中,欢迎大家一起学习和探讨。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
元组来定位到要读数据的位置。

最后

Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。

人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。

资源持续更新中,欢迎大家一起学习和探讨。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值