C/C++后端开发面经(1)——计算机操作系统

C/C++后端开发面经(1)——计算机操作系统

1.1 进程线程的基本概念

1.1.1 什么是进程,线程?

进程是资源(CPU、内存等)分配的基本单位,线程是CPU调度和分配的基本单位(程序执行的最小单位)。

  1. 当我们运行一个程序的时候,系统就会创建一个进程,并分配地址空间和其他资源,最后把进程加入就绪队列直到分配到CPU时间就可以正式运行了。
  2. 线程是进程的一个执行流,有一个初学者可能误解的概念,进程就像一个容器一样,包括程序运行的程序段、数据段等信息,但是进程其实是不能用来运行代码的,真正运行代码的是进程里的线程
  3. 那么,来看看我们最熟悉的main()函数,我们既可以认为这是一个进程,也可以认为是一个线程。我们都知道,在C/C++中main函数是程序入口,所以准确来说main函数是程序的主线程。然而很神奇的地方在于,当系统在执行main函数的时候,main函数又是一个独立的进程,我们可以在main函数里创建子进程,也可以创建子线程。
  4. 在main函数里创建的多个子线程中,每个线程有自己的堆栈和局部变量,但多个线程也可共享同个进程下的所有共享资源,因此我们经常可以创建多个线程实现并发操作,实现更加复杂的功能。

示例:我们看一个实际例子来加强理解。

int g_cnt = 0;  //全局变量
int * thread(void * arg)
{
   
    int m_cnt = 0;
    m_cnt = 5;
    g_cnt++;
    return 0;
}
int main(void)
{
   
    int err = 0;
    pthread_t tid;
    int m_cnt = 0;
    err=pthread_create(&tid, NULL, thread, NULL);  //创建子线程
    if (0 != err)   //检验是否创建成功
    {
   
        printf("can't creat thread: %s\n", strerror(err));
    }
    while(g_cnt == 0)
    {
   
        usleep(300);   //延迟300毫秒,让子线程运行一会儿
    }
    printf("g_cnt = %d, m_cnt = %d\n", g_cnt,  m_cnt);
    return 0;
}

我们可以看出main函数是一个主线程,开始执行程序,同时main函数又是一个进程,我们可以创建子线程thread(),子线程有自己的堆栈和局部变量,同时又与主线程共享全局变量,这也就是为何输出结果显示子线程改变了全局变量g_cnt,但没有改变main函数里的同名局部变量m_cnt。
在这里插入图片描述

还有一个关键点需要注意:我们可以看到main函数有一个while循环,一开始g_cnt等于里,程序进入while循环后就不能做其他事情,但是子线程thread不受影响,仍然可以独立于main函数,自己做自己的事情。

1.1.2 多进程、多线程的优缺点

解析:为了理解多进程、多线程各自的优缺点之前,我们需要先了解进程和线程最大的区别和联系,一个进程由PCB(进程控制块)、数据段、代码段组成,进程本身不可以运行程序,而是像一个容器一样,先创建出一个主线程,分配给主线程一定的系统资源,这时候就可以在主线程开始实现各种功能。当我们需要实现更复杂的功能时,可以在主线程里创建多个子线程,跟人多好干活的道理一样,多个线程在同一个进程里,利用这个进程所拥有的系统资源合作完成某些功能。

  1. 多进程更健壮,一个进程死了不影响其他进程,子进程死了也不会影响到主进程,毕竟系统会给每个进程分配独立的系统资源。多线程比较脆弱,一个线程崩溃很可能影响到整个程序,因为多个线程是在一个进程里一起合作干活的。
  2. 进程性能大于线程,每个进程独立地址空间和资源,而多个线程是一起共享了同个进程里的空间和资源,结果就很明显了,线程的性能上限一定比不上进程。
  3. 正因为进程性能大于线程。所以这也引发了另一重要知识点,创建多进程的系统花销远大于创建多线程
  4. 多进程通讯因为需要跨越进程边界,不适合大量数据的传送,更适合小数据或者密集数据的传送。而多线程无需跨越进程边界,适合各线程间大量数据的传送,甚至还有很重要的一点,多线程可以共享同一进程里的共享内存和变量哦。
  5. 多进程逻辑控制比多线程复杂,需要与主进程做好交互。根据上面几点,我们不难知道多进程是“要用来做大事”的,而多线程是“各自做件小事,合作完成大事”。所以要做大事自然就需要更复杂的逻辑控制,不像做小事那么目标明显。
  6. 虽然多线程逻辑控制比较简单,但是却需要复杂的线程同步和加锁控制等机制,而进程就不需要了。
  7. 最后的一点,可能比较少见,我们可以通过增加CPU的数量来增加进程的数量,但增加不了线程的数量,即增加CPU无法提高线程数量,线程数量由进程的空间资源和线程本身栈大小确定,详情见1.1.6小节。

1.1.3 什么时候用进程,什么时候用线程

解析:还是同一个思想,进程是“要用来做大事”的,而线程是“各自做件小事,合作完成大事”,解析结合上节新鲜出炉的优缺点我们就很好理解什么时候用进程或者线程了。

  1. 创建和销毁较频繁使用线程,因为创建进程花销大嘛。
  2. 需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
  3. 并行操作使用线程。线程是为了实现并行操作的一个手段,也就是刚才说的需要多个并行操作“合作完成大事”,当然是使用线程啦。
  4. 最后可以总结为:安全稳定选进程;快速频繁选线程

1.1.4 单线程和多线程区别

  1. 多线程好处:
    可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
  2. 多线程的不利方面:
    线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
    多线程需要协调和管理,所以需要CPU时间跟踪线程;
    线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
    线程太多会导致控制太复杂,最终可能造成很多Bug;

1.1.5 多进程、多线程同步(通讯)的方法

当我们在使用系统编程时,就会遇到多进程、多线程编程,所以必须知道在多个进程、多个线程之间都有什么常见的通讯机制,这也是嵌入式面试中高频问题之一。

1.1.5.1 进程间通讯

进程间通讯:
(1)管道/无名管道(2)信号(3)共享内存(4)消息队列(5)信号量(6)socket
注意:临界区则是一种概念,指的是访问公共资源的程序片段,并不是一种通信方式。

  1. 管道,通常指无名管道。
    1. 半双工,具有固定的读端和写端;
    2. 只能用于具有亲属关系的进程之间的通信;
    3. 可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write函数。但是它不是普通的文件,并不属于其他任何文件系统,只能用于内存中。
      4)Int pipe(int fd[2]); 当一个管道建立时,会创建两个文件文件描述符,要关闭管道只需将这两个文件描述符关闭即可。
  2. FiFO(有名管道)
    1. FIFO可以再无关的进程之间交换数据,与无名管道不同;
    2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中;
    3. Int mkfifo(const char pathname,mode_t mode);*
  3. 消息队列
    1. 消息队列,是消息的连接表,存放在内核中。一个消息队列由一个标识符来标识;
    2. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
    3. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除;
    4. 消息队列可以实现消息的随机查询
  4. 信号量
    1. 信号量是一个计数器,信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据;
    2. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存;
    3. 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作;
  5. 共享内存
    1. 共享内存,指两个或多个进程共享一个给定的存储区;
    2. 共享内存是最快的一种进程通信方式,因为进程是直接对内存进行存取;
    3. 因为多个进程可以同时操作,所以需要进行同步;
    4. 信号量+共享内存通常结合在一起使用。
1.1.5.2 线程通讯

线程通讯(同步):
(1)信号量(2)读写锁(3)条件变量(4)互斥锁(5)自旋锁

多线程通过特定的设置来控制线程之间的执行顺序,也可以说在线程之间通过同步建立起执行顺序的关系;
主要四种方式:临界区、互斥对象、信号量、事件对象;其中临界区和互斥对象主要用于互斥控制信号量和事件对象主要用于同步控制

  1. 临界区:
    通过对多线程的串行化来访问公共资源或一段代码,速度快、适合控制数据访问。在任意一个时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
  2. 互斥对象:
    互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。
  3. 信号量:
    它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最 大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号。但是当前可用计数减小 到0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离 开的同时通过ReleaseSemaphore ()函数将当前可用资源计数加1 。在任何时候当前可用资源计数决不可能大于最大资源计数。
  4. 事件对象:
    通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。

1.1.6互斥锁与信号量的区别?

互斥锁用于线程的互斥,信号量用于线程的同步。这是互斥锁和信号量的根本区别,也就是互斥和同步之间的区别。同时互斥锁的作用域仅仅在于线程,信号量可以作用于线程和进程。

1.1.7 进程的空间模型

解析:32位系统中,当系统运行一个程序,就会创建一个进程,系统为其分配4G的虚拟地址空间,其中0-3G是用户空间,3-4G是内核空间,具体如图所示,内核空间是受保护的,用户不能对该空间进行读写操作,否则可能出现段错误。其中栈空间有向下的箭头,代表数据地址增加的空间是往下的,新的数据的地址的值反而更小,堆空间则是往上
在这里插入图片描述

  1. 栈区:由编译器自动分配和释放,存放函数的参数值(形参)、局部变量(int a =1;还有指针变量)等,其操作方式类似于数据结构中的栈,先进后出。
  2. 堆区:一般由程序员分配和释放,若程序员不释放,可能会造成内存泄漏,程序结束的时候可能由操作系统回收,注意它与数据结构中的堆是两回事,分配方式类似于链表。
  3. 全局区(静态区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域 (.data)未初始化的全局变量和未初始化的静态变量在相邻的另一块区域 (.bss),程序结束后系统释放。
  4. 文字常量区:常量字符串放在这里,程序结束后有系统释放。
  5. 程序代码区(.text):存放函数体的二进制代码。

栈的空间有限,堆是很大的自由存储区,程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也是在栈上进行。
注意:64位操作系统下的虚拟内存空间大小:地址空间大小不是232 , 也不是264,而一般是248。因为并不需要264那么大的寻址空间,过大的空间只会造成资源的浪费。所以64位Linux一般使用48位表示虚拟空间地址,40位标识物理地址。
0x0000000000000000 ~ 0x00007fffffffffff表示用户空间,
0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF表示内核空间,共提供256TB(248)的寻址空间。

1.1.8 静态变量和全局变量的区别

全局变量与全局静态变量的区别:
(1)若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
(2)若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享, 即:全局静态变量对组成该程序的其它源文件是无效的。

静态全局变量的作用:
(1)不必担心其它源文件使用相同变量名,彼此相互独立。
(2)在某源文件中定义的静态全局变量不能被其他源文件使用或修改。

1.1.9 一个进程可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

何蔚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值