四、IO进程:IPC
1. 标准IO和系统IO的区别
标准文件IO
概念:C库中定义的一组用于输入输出的函数
特点
(1)有缓存机制,减少系统调用
(2)围绕文件流进行操作
(3)默认打开三个文件流,stdin、stdout、stderr
(4)一般只对普通文件进行操作
系统文件IO
概念:系统中定义的一组用于输入输出的函数
特点
(1)无缓存机制,每次都要系统调用
(2)围绕文件描述符进行操作
(3)默认打开三个文件描述符,0、1、2
(4)可以对任意类型的文件操作,不能操作目录
2. 静态库和动态库的区别
静态库和动态库,本质区别是代码被载入时刻不同。
- 静态库:在程序编译时会被连接到目标代码中。
-
- 优点:程序运行时将不再需要该静态库,运行速度更快,移植性更好 ;
- 缺点:静态库中的代码复制到了程序中,因此体积较大;静态库升级后,程序需要重新编译链接
- 动态库:在程序运行被载入代码中。
-
- 优点:程序在执行时加 载动态库,代码体积小;程序升级更简单;不同应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。
- 缺点:运行时还需要动态库的存在,移植性较差
3. 怎么创建进程
使用fork()函数创建一个新的进程 (子进程)。fork()会复制当前进程,并创建一个新的子进程。当调用fork()时,当前的进程被复制,产生一个新的进程,称为子进程。父进程和子进程几乎完全相同,但它们各自拥有不同的进程ID(PID)。
fork() 的基本操作过程
- 父进程调用 fork(),操作系统为子进程分配独立的进程控制块(PCB)。
- 操作系统会将父进程的地址空间拷贝到子进程,但实际内存是共享的,直到子进程或父进程发生写操作(COW机制)。
- 父子进程开始各自执行,各自拥有独立的进程ID、堆栈和其他资源。
写时拷贝 Copy-On-Write, COW
- 写时复制的原理是:当父进程和子进程运行时,它们共享相同的物理内存页面。只有当父进程或子进程试图写入内存时,操作系统才会为该进程创建独立的内存页面,避免二者互相影响。
- 这种方式极大地减少了 fork() 时的内存开销,因为在许多情况下,父进程和子进程只读共享的内存数据。
4. 什么是守护进程
概念: 在后台运行、不直接与用户交互的进程。它是脱离控制终端且周期执行的进程。它通常在系统启动时自动启动,执行一些系统或服务相关的任务,并且在整个系统运行期间持续运行,直到系统关闭或手动终止它。守护进程可以提供各种服务,如日志记录、网络服务、作业调度等。
如何创建一个守护进程
1) 创建子进程,父进程退出
2) 在子进程中创建新会话
3) 改变进程运行路径为根目录
4) 重设文件权限掩码
5) 关闭其他不必要的文件描述符
5. 什么是僵尸进程?什么是孤儿进程?
- 僵尸进程: 子进程先结束,父进程如果没有及时回收,子进程变成僵尸进程(要避免僵尸进程产生)。
- 孤儿进程: 若父进程先结束,子进程成为孤儿进程,被init进程收养,会让子进程变成后台进程。
避免僵尸进程产生的方法
- 使用wait()或waitpid()系统调用:父进程应该在子进程结束后及时调用wait()或waitpid()系统调用来回收子进程的资源。这样可以确保子进程的进程描述符被及时释放,从而避免僵尸进程的产生。
- 设置SIGCHLD信号的处理函数:父进程可以设置一个SIGCHLD信号的处理函数,当子进程结束时,该信号会被发送到父进程。在信号处理函数中,父进程可以调用wait()或waitpid()来回收子进程的资源。
- 忽略SIGCHLD信号:如果父进程不关心子进程的退出状态,可以选择忽略SIGCHLD信号。这样,当子进程结束时,内核会自动回收子进程的资源,不会产生僵尸进程。但需要注意的是,这种方法会使得父进程无法获取子进程的退出状态。
6. 时间片了解么?
- 时间片是操作系统中用于调度进程的基本单位。在多道程序设计和多任务处理中,操作系统将 CPU 的执行时间划分成一小段小的时间片,每个进程在一个时间片内运行,然后切换到下一个进程。
- 时间片轮转算法是一种常见的调度算法,它将每个进程分配一个时间片,当时间片用完后,操作系统会暂停当前进程的执行,并将 CPU 分配给下一个就绪的进程,然后继续执行下一个时间片。这样,所有进程会依次获得 CPU 的执行机会,从用户的角度看,它们似乎是同时运行的。
- 时间片的大小通常由操作系统调度器决定,可以根据系统的性能、负载和策略来调整。如果时间片太小,会导致频繁的进程切换,增加系统开销;如果时间片太大,可能会导致长时间运行的进程占用 CPU 时间过长,导致其他进程响应变慢。
7. 进程与线程的共性和区别?
- 共性
-
- 动态性:进程和线程都是动态的概念,它们的存在和消亡都是动态的,具有一定的生命周期。
- 并发性:进程和线程都可以并发执行,即多个进程或多个线程可以同时存在于内存中,并在操作系统的调度下交替执行。
- 独立性:进程和线程在各自的执行空间内是相对独立的,它们拥有各自的数据结构和堆栈空间,互不干扰。
- 资源分配:无论是进程还是线程,操作系统都会为它们分配必要的资源,如内存、文件、I/O设备等。
- 区别
-
- 资源开销:进程是资源分配的基本单位,它拥有独立的内存空间和系统资源,因此创建和销毁进程的开销相对较大。而线程是处理器调度的基本单位,它共享进程的资源,因此创建和销毁线程的开销较小。
- 执行过程:进程是独立执行的,它拥有一个完整的执行环境。而线程是在进程内部执行的,多个线程共享进程的内存空间和资源。
- 通信机制:进程间通信(IPC)需要操作系统提供特殊的机制,如管道、消息队列、共享内存等。而线程间通信则相对简单,因为它们共享进程的内存空间,可以直接读写共享变量或使用简单的同步机制。
- 稳定性:进程是操作系统分配资源的基本单位,因此进程崩溃通常不会影响到其他进程。而线程是进程的一部分,一个线程的崩溃可能导致整个进程的崩溃。
8. 线程的同步怎么实现
同步:在互斥的基础上按约定好的顺序对临界资源进行操作
实现同步的机制:信号量、互斥锁+条件变量
9. 线程的互斥怎么实现
互斥:同一时间只有一个线程对临界资源进行操作
实现互斥的机制:互斥锁
10. 进程间通信方式有哪些?哪种效率最高
- 无名管道、有名管道、信号量、共享内存、信号灯集、消息队列、套接字
- 效率最高是共享内存:
共享内存效率最高的主要原因是它避免了数据的复制和传输。在共享内存中,多个进程可以直接访问同一块物理内存,无需进行数据的拷贝和传输,因此在数据量较大的情况下,共享内存的效率要远高于其他进程间通信方式。
具体来说,以下是共享内存效率高的几个关键点:
-
- 避免数据拷贝:在其他进程间通信方式(如管道、消息队列等)中,数据需要从一个进程的地址空间复制到内核缓冲区,然后再从内核缓冲区复制到另一个进程的地址空间,涉及两次数据拷贝操作。而在共享内存中,多个进程共享同一块物理内存,不需要进行数据拷贝,直接读写共享内存即可。
- 高效的数据访问:共享内存的数据直接位于物理内存中,而其他进程间通信方式的数据位于内核缓冲区,因此在共享内存中访问数据更加高效。
- 无需内核介入:在共享内存中,数据的读写不需要内核的介入,减少了系统调用和上下文切换,提高了通信的速度和效率。
11. 进程间通信方式的优缺点对比
- 共享内存:
优点:速度快、低开销、适合大数据量。
缺点:需要额外的同步机制、内存管理复杂。
- 无名管道(Pipes):
优点:简单易用,适合父子进程。
缺点:有缓冲区限制,效率较低。
- 命名管道(FIFO):
优点:可在无亲缘关系的进程间使用,易于实现。
缺点:也有缓冲区限制,效率相对较低。
- 消息队列:
优点:支持优先级,灵活性高,可以按照类型添加读取消息。
缺点:内核管理开销大,可能会满。
- 信号:
优点:简单易用,用于进程同步。
缺点:只用于信号,不传输数据。
- 套接字(Sockets):
优点:支持网络通信,跨主机灵活性高。
缺点:开销大,特别是本地通信时,实施复杂。
12. 有名管道和无名管道的区别?
- 无名管道
具有亲缘关系的进程间。
半双工通信方式,有固定的读端和写端,看做一种特殊文件,通过文件描述符操作 。
管道中没有数据时,读阻塞;管道中写满数据时,写阻塞;读端关闭,往管道写数据导致管道破裂。
- 有名管道
互不相关的两个进程间。
在路径中存在管道文件,数据存放在内核空间。
遵循先进先出,不支持lseek。
只写方式,写阻塞,一直到另一个进程把读打开;只读方式,读阻塞,一直到另一个进程把写打开;可读可写,如果管道中没有数据,读阻塞。
13. 共享内存的实现方式
- 创建共享内存:
首先,需要创建一块共享内存区域。在 Linux 系统中,可以使用 shmget 系统调用来创建或获取共享内存。需要提供一个唯一的键值来标识共享内存区域,并可以指定一些标志和权限来控制内存区域的属性。
- 映射共享内存:
一旦共享内存区域被创建,进程可以使用 shmat 系统调用将共享内存区域关联到自己的地址空间中。需要提供共享内存的标识符,如果有多块共享内存区域,可以通过指定不同的标识符来关联不同的区域。关联后,进程就可以直接访问共享内存中的数据。
- 使用共享内存:
一旦共享内存区域被关联到进程的地址空间中,进程可以像访问普通内存一样直接读写共享内存中的数据。多个进程可以共享同一块物理内存,无需进行数据的拷贝和传输,提高了通信的效率。
- 解除关联:
当不再需要共享内存时,进程可以使用 shmdt 系统调用将共享内存区域从自己的地址空间中解除关联。解除关联后,进程将无法再访问共享内存中的数据。
- 删除共享内存:
当所有进程都解除了对共享内存的关联,可以使用 shmctl 系统调用删除共享内存区域,释放相关资源。需要注意,删除共享内存只是将共享内存标记为删除状态,实际的内存回收会在所有进程都解除关联后进行。
14. 消息队列的实现方式
- 创建消息队列:
需要创建一个唯一的值key键来标识该消息队列,并可以指定一些标志来控制队列的属性。
使用系统调用(如 msgget)来创建一个消息队列。如果消息队列已经存在,可以通过指定相同的键值获取已有的消息队列。
- 发送消息到队列:
使用系统调用(如 msgsnd)将消息发送到队列中。需要指定消息队列的标识符、要发送的消息内容、消息的大小和一些标志。发送的消息需要封装成特定格式,包括消息类型和实际数据。
- 接收消息:
其他进程可以使用系统调用(如 msgrcv)从消息队列中接收消息。需要指定消息队列的标识符、用于接收消息的缓冲区、缓冲区的大小、要接收的消息类型和一些标志。如果队列中没有符合条件的消息,接收进程可以选择阻塞或立即返回。
- 删除消息队列:
当不再需要消息队列时,可以使用系统调用(如 msgctl)将其删除,释放相关资源。删除消息队列前,需要确保所有进程都已经停止使用该队列。
15. fork和vfork区别
fork 和 vfork 是两种在 Linux/Unix系统中用于创建新进程的系统调用,它们有一些区别:
- fork:
fork 是创建新进程的标准方法,它会复制当前进程的所有资源(包括代码、数据、堆栈等),创建一个与父进程几乎完全相同的子进程。
父进程和子进程之间的地址空间是独立的,互相不会影响。子进程从 fork 调用之后的位置开始执行,可以通过返回值来判断当前进程是父进程还是子进程。
在 fork 调用后,父进程和子进程并发执行,并共享一些资源,比如打开的文件描述符、信号处理器等。
父进程和子进程的执行顺序是不确定的,取决于系统调度器的调度策略。
- vfork:
vfork 也是用于创建新进程的系统调用,但它与 fork 有一些不同之处。
vfork 创建的子进程与父进程共享地址空间,即子进程与父进程共用同一份数据和代码。这意味着在子进程中执行的任何修改都会直接影响到父进程,因此 vfork 的使用要非常小心。
vfork 是为了在子进程中立即执行一个新程序(通常是通过 exec 系统调用),然后立即退出,不需要复制父进程的地址空间,以节省资源。
在 vfork 调用后,父进程会被阻塞,直到子进程执行完毕或调用 exec 系统调用后才会继续执行。
总的来说,fork 是创建新进程的常规方法,而 vfork 适用于在子进程中立即执行一个新程序。在使用 vfork 时需要格外小心,避免对父进程的地址空间造成破坏。通常情况推荐使用 fork 来创建新进程。
16. 线程的死锁,怎么避免?
死锁:
是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成 的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
死锁产生的四个必要条件:
(1)互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
(2) 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
(3)请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
(4)循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
注意:当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
解决方法
(1)避免循环等待:确保线程获取资源的顺序是一致的,避免形成循环等待的情况
(2)使用资源分级:将资源分为不同的等级或优先级,确保线程按照一定的顺序获取资源
(3)使用超时机制:在获取资源时,设置一个超时时间,并在超过时间限制后放弃获取资源并释放已持有的资源
(4)避免资源的重复占用:尽量避免在一个线程中重复获取已经持有的资源
(5)合理规划资源的利用
(6)使用死锁检测和恢复机制