秋招总结(三)-操作系统归纳

1.进程和线程的区别

  • 根本区别:进程是资源分配最小单位,线程是程序执行的最小单位;

  • 开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 内存方面:进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,每个进程都有代码段、堆栈段和数据段;线程没有独立的地址空间,同一个进程内的所有线程共享同一个进程的内存空间,它使用相同的地址空间共享数据;

  • 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行

  • 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,但多线程程序处理好同步与互斥是个难点;进程之间的通信需要以进程通信的方式(IPC)进行;

  • 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);

知乎CPU层面的解释

 

2.线程的五种状态以及转换

  • 新建状态(New):新创建了一个线程对象。
  • 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,等待获取CPU的使用权即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
  • 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
  • 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
  • 阻塞的情况分三种:
    • 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
    • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
    • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

 

 

 

3.进程间通信(IPC)

进程同步与进程通信很容易混淆,它们的区别在于:

进程同步:控制多个进程按一定顺序执行;

进程通信:进程间传输信息。

进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。


进程间通信方式:

  • 无名管道
    • 无名管道是通过调用 pipe 函数创建的,pipe(int fd[2]) : fd[0] 用于读,fd[1] 用于写 
    • 只支持半双工通信(单向交替传输);
    • 无名管道只能用于父子进程或兄弟进程之间,必须用于具有亲缘关系的进程间的通信。
  • FIFO 命名管道
    • 有名管道是FIFO文件,存在于文件系统中,可以通过文件路径名来指出。
    • 有名管道可以在不具有亲缘关系的进程间进行通信。
    • FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
  • 消息队列
    • 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
    • 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
    • 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
    • 消息队列的主要特点是异步处理,主要目的是减少请求响应时间和解耦。

主要的使用场景就是将比较耗时而且不需要立即生效返回结果的操作,我们把这种操作作为一个消息,放到消息队列中。处理方可以在任何时候去获取并处理这条消息。这里我们只要保证消息的格式不变,消息的发送方和接收处理方都认识这个消息,那么双方就不需要彼此通信,即可以完成一件事。

  • 信号量

    • 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

    • “二进制信号量”:信号量的值在0和1之间跳转,实现同步互斥

  • 共享内存

    • 进程可以将同一段共享内存连接到它们自己的地址空间,所有进程都可以访问共享内存中的地址,如果某个进程向共享内存内写入数据,所做的改动将立即影响到可以访问该共享内存的其他所有进程。
    • 因为数据不需要在进程之间复制,所以这是最快的一种 IPC

    • 相关接口:
      • 创建共享内存:int shmget(key_t key, int size, int flag);
        • 成功时返回一个和key相关的共享内存标识符,失败范湖范围-1。
        • key:为共享内存段命名,多个共享同一片内存的进程使用同一个key。
        • size:共享内存容量。
        • flag:权限标志位,和open的mode参数一样。
      • 连接到共享内存地址空间:void *shmat(int shmid, void *addr, int flag);
        • 返回值即共享内存实际地址。
        • shmid:shmget()返回的标识。
        • addr:决定以什么方式连接地址。
        • flag:访问模式。

共享内存的优势也很明显,首先可以通过共享内存进行通信的进程不需要像无名管道一样需要通信的进程间有亲缘关系。其次内存共享的速度也比较快,不存在读取文件、消息传递等过程,只需要到相应映射到的内存地址直接读写数据即可。

 

 

4.进程同步

  • 临界区
    • 对临界资源进行访问的那段代码称为临界区
    • 为了互斥访问临界资源,每个进程在进入临界区之前都需要先进行检查
  • 同步与互斥 (mutex)
    • 同步:多个进程按一定顺序执行
    • 互斥:多个进程在同一时刻只有一个进程能进入临界区
  • 信号量
    • 信号量是一个整型变量,可以对其执行down和up操作,也就是常见的P和V操作。
    • 二进制0/1的信号量可以实现互斥量(mutex)
  • 管程
    • 管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具,但是具有与信号量及PV操作不同的属性。
    • 引入原因:信号量机制的缺点:进程自备同步操作,P(S)和V(S)操作大量分散在各个进程中,不易管理,易发生死锁。
    • 管程机制的目的:1、把分散在各进程中的临界区集中起来进行管理;2、防止进程有意或无意的违法同步操作;3、便于用高级语言来书写程序,也便于程序正确性验证。

 

 

5.C++线程中的几种锁

  • 互斥锁 (Mutex)
    •  相应接口:mutex.lock() / mutex.unlock();   C++中提供std:lock_guard类模板,实现了互斥元的RAII;
    • mutex t_mutex

      lock_guard<mutex> guard(t_mutex); 

    • 在某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。
  • 条件锁/条件变量(Condition)
    • 相应接口:类型condition_variable condition, condition.wait(), condition.notify()
    • 条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。
  • 自旋锁
    • 从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。
    • 当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道“自旋锁”是比较耗费CPU的。

假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。

首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。

而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。

  • 读写锁
    • 借助“读者-写者”问题理解:
      • 计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。这就是一个简单的读者-写者模型。
      • 相关接口:类型boost::shared_mutex
        • 独占加锁:std::lock_guard< boost::shared _mutex>和 std::unique _lock< boost::shared _mutex>
        • 共享访问:boost::shared _lock< boost::shared _mutex>来获得共享访问
      • 唯一的限制是,如果任意一个线程拥有一个共享锁,试图获取独占锁的线程会被阻塞,知道其他线程全都撤回它们的锁。同样的,如果一个线程具有独占锁,其他线程都不能获取共享锁或独占锁,直到第一个线程撤回它的锁。

 

 

6.僵尸进程、孤儿进程、守护进程

  • 孤儿进程:父进程在调用fork接口之后和子进程已经可以独立开,之后父进程和子进程就以未知的顺序向下执行(异步过程)。所以父进程和子进程都有可能先执行完。当父进程先结束,子进程此时就会变成孤儿进程,不过这种情况问题不大,孤儿进程会自动向上被init进程收养,init进程完成对状态收集工作。而且这种过继的方式也是守护进程能够实现的因素。
  • 僵尸进程:如果子进程先结束,父进程并未调用wait或者waitpid获取进程状态信息,那么子进程描述符就会一直保存在系统中,这种进程称为僵尸进程。
  • 守护进程:守护进程是脱离终端并在后台运行的进程,执行过程中信息不会显示在终端上并且也不会被终端发出的信号打断。
    • 守护进程创建步骤:
      1. 创建子进程,终止父进程:fork()+if(pid > 0) exit(0);   使子进程成为孤儿进程被init进程收养
      2. 在子进程中创建新会话:setsid() 让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制
      3. 改变工作目录:chdir("/");  因为子进程也继承了父进程的工作目录,所有把工作目录换成其它路径
      4. 重设文件掩码:umask(0),文件创建掩码设置为0,可以大大增强该守护进程的灵活性。umask是缺省权限,umask(0),文件权限即为666,目录权限为777; (对于文件来说创建时不具有执行权限,所以默认去掉);
      5. 关闭文件描述符:用fork新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载。

 

7.select、epoll的区别

  • 操作方式及效率:select返回的是含有所有句柄的数组,需要遍历fd_set每一位才能发现哪些句柄发生了事件,O(n); poll也是遍历,但只遍历到pollfd数组当前已使用的最大下标; epoll是回调,O(1);
  • 最大连接数:select为1024/2048(单个进程打开的文件数有限制); poll和epoll无上限
  • fd拷贝:select和poll每次都需要把fd集合从用户态拷贝到内核态;epoll调用epoll_ctl时拷贝进内核并放到事件表中,但用户进程和内核通过mmap映射共享同一块存储,避免了fd从内核赋值到用户空间;
  • 其它: select每次内核仅仅是通知有消息到了需要处理,具体是哪一个需要遍历所有的描述符才能找到。epoll不仅通知有I/O到来还可通过callback函数具体定位到活跃的socket,实现伪AIO。

 

epoll原理:

  • epoll_create:内核在epoll文件系统建立file节点;内核cache里建立一个红黑树用于存储以后注册的socket fd; 建立一个就绪句柄链表,存储准备就绪的事件;
  • epoll_ctl:若socket句柄在红黑树中存在则立即返回; 不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
  • epoll_wait:若list链表中有数据,则立即返回; 没有数据就sleep,等到timeout时间到后链表没数据也返回;

 

LT水平触发和ET边缘触发的区别:

LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时重复返回这个句柄,而ET模式仅在第一次返回。

两种模式的实现:

当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait检查这些socket,如果是LT模式,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。所以,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式如果一次没有把数据全部读写完,那么下次调用epoll_wait时,也不会添加到准备就绪链表中,直到该socket句柄出现第二次可读写事件才通知;

ET效率更高:由此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见; 

8.常用的Linux命令

  • 1. find:查找文件
  • 2. grep:查找文件中的行,常用来在日志中找信息
  • 3. awk:相当强大的工具,用来统计日志信息
  • 4. sed:按行编辑文件,常用来处理配置文件
  • 5. netstat:查看网络信息
  • 6. ps:用来查看进程状态
  • 7. top:  监控linux系统状况; CPU、内存的使用
  • 8.lsof: 查看所有打开的文件;   -u 某个用户打开的文件;  -c  或者 -p+进程号  查看某个进程打开的文件

 

 

9.操作系统的划分

  • 批处理系统
    • 批处理系统,又名批处理操作系统。批处理是指用户将一批作业提交给操作系统后就不再干预,由操作系统控制它们自动运行。这种采用批量处理作业技术的操作系统称为批处理操作系统。批处理操作系统分为单道批处理系统和多道批处理系统。批处理操作系统不具有交互性,它是为了提高CPU的利用率而提出的一种操作系统。

  • 交互式系统 (分时系统)
    • 交互式操作系统是为达到人机交互目的而为机器所编写的操作系统。人机交互是指人与计算机之间使用某种对话语言,以一定的交互方式,为完成确定任务的人与机器之间的信息交换过程。

      常见的交互式操作系统具体有Windows、DOS等,而操作系统三大类型中的分时操作系统也称为“事务处理使用的交互式操作系统”.

  • 实时系统
    • 实时系统要求一个请求在一个确定时间内得到响应。

      分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

  • 嵌入式系统

 

10.进程调度算法

  • 批处理系统
    1. 先来先服务:按照请求的顺序进行调度。有利于长作业,但不利于短作业
    2. 短作业优先:按估计运行时间最短的顺序进行调度。长作业可能饿死,一直等待短作业执行完毕
    3. 最短剩余时间优先:按估计剩余时间最短的顺序进行调度。
  • 交互式系统
    1. 时间片轮转:将所有就绪进程按到来的先后顺序排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。时间片轮转算法的效率和时间片的大小有很大关系
    2. 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
    3. 多级反馈队列:多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

       

  • 实时系统
    1. 实时系统要求一个请求在一个确定时间内得到响应。

      分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。

 

 

11.死锁产生的必要条件

  • 互斥:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  • 占有和等待:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  • 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
  • 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。

 

12.协程

  • 先讲下进程和线程,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。线程从属于进程,是程序的实际执行者。对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。
  • 线程是比进程更轻量的存在,但同样线程状态切换、线程上下文切换、同步锁这些都是耗费性能的操作;
  • 协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
  • 协程的总结:
  1. 协程属于线程,即一个线程下面可以开辟多个协程。
  2. 协程是用户态的轻量级线程。
  3. 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
  4. 当多个协程切换时,由于其同属于一个线程,所以可以看作是同步执行的,不存在同时共享资源的情况,可以不加锁的访问全局变量,切换上下文非常快。

 

 

13.用户态和内核态

  • linux进程有4GB地址空间,如图所示:
  • 3G-4G大部分是共享的,是内核态的地址空间。这里存放整个内核的代码和所有的内核模块以及内核所维护的数据。

  • 特权级概念:对于任何操作系统来说,创建一个进程是核心功能。创建进程要做很多工作,会消耗很多物理资源。比如分配物理内存,父子进程拷贝信息,拷贝设置页目录页表等等,这些工作得由特定的进程去做,所以就有了特权级别的概念。最关键的工作必须交给特权级最高的进程去执行,这样可以做到集中管理,减少有限资源的访问和使用冲突。inter x86架构的cpu一共有四个级别,0-3级,0级特权级最高,3级特权级最低。
  • 用户态和内核态的概念:
  1. 用户态:当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。
  2. 内核态:用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,然后进入3G-4G中的内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据。
  • 用户态和内核态的切换:当在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成一些用户态自己没有特权和能力完成的操作时就会切换到内核态。用户态切换到内核态的3种方式:
  1. 系统调用:这是用户态进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。例如fork()就是执行了一个创建新进程的系统调用。系统调用的机制和新是使用了操作系统为用户特别开放的一个中断来实现;
  2. 异常:当cpu在执行运行在用户态下的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。
  3. 外围设备中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

 

14.内存溢出和内存泄漏的区别

  • 内存溢出 out of memory: 是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
  • 内存泄露 memory leak: 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak会最终会导致out of memory!

内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。

内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出. 

 

以发生的方式来分类,内存泄漏可以分为4类: 

  • 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
  • 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  •  一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。 
  • 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。 
     

 

15.Linux内存管理

操作系统内存管理

内存寻址

内存管理

 

 

16.静态链接和动态链接

静态链接:

目标文件概念:

可执行目标文件:可以直接在内存中执行;
可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;
共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;
 

静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。

链接器主要完成一下两个任务:

符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

                                            

 

静态库有以下两个问题:

当静态库更新时那么整个程序都要重新进行链接;
对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。
 

动态链接:

共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,具有以下特点:

在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
动态库运行时会先检查内存中是否已经有该库的拷贝,若有则共享拷贝,否则重新加载动态库(C语言的标准库就是动态库)。静态库则是每次在编译阶段都将静态库文件打包进去,当某个库被多次引用到时,内存中会有多份副本,浪费资源。

动态库另一个有点就是更新很容易,当库发生变化时,如果接口没变只需要用新的动态库替换掉就可以了。但是如果是静态库的话就需要重新被编译。

不过静态库也有优点,主要就是静态库一次性完成了所有内容的绑定,运行时就不必再去考虑链接的问题了,执行效率会稍微高一些。

 

 

 

 

 

 

 

 

©️2020 CSDN 皮肤主题: 像素格子 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值