一、选择题(每题2分,总分30分)
1. 下列不是用户进程的组成部分的是( D )
[A] 正文段 [B] 用户数据段 [C] 系统数据段 [D] elf段
根据进程的基本概念,进程是由正文段、用户数据段以及系统数据段共同组成的一个执行环境。其中:
- 正文段(也称为代码段):存放被执行的机器指令,这部分是只读的,允许正在运行的两个或多个进程之间共享这一代码。
- 用户数据段:存放进程在执行时直接进行操作的所有数据,包括进程使用的全部变量在内,这部分的信息可以被改变。每个进程需要有自己的专用用户数据段。
- 系统数据段:有效地存放程序运行的环境,包括进程的控制信息等。
接下来,我们来看选项D中的"elf段"。ELF(Executable and Linkable Format)是一种用于可执行文件、目标代码、共享库以及核心转储的标准文件格式。虽然ELF文件与进程的执行有关,但"elf段"本身并不是用户进程的组成部分。
【正确答案:[D] elf段】
2. 以下哪种不是进程的类型 ( )
[A] 批处理进程 [B] 管理进程 [C] 交互进程 [D] 守护进程
回顾一下进程的类型:
- [A] 批处理进程:这是与Windows原来的批处理类似的进程,是一个进程序列,负责按照顺序启动其他进程。
- [C] 交互进程:这是由shell启动的进程,既可以在前台运行,也可以在后台运行。在执行过程中,它要求与用户进行交互操作。
- [D] 守护进程:这是执行特定功能或执行系统相关任务的后台进程。它不是内核的组成部分,但在系统启动时可能启动,并在系统关闭前持续运行。
选项B:管理进程。
在操作系统的上下文中,虽然有一个进程管理系统(负责创建、调度、终止进程等),但“管理进程”本身并不是一个特定的进程类型。它是一个更广泛的概念,涉及多个进程和组件,用于管理系统的各种资源。
【正确答案:[B] 管理进程】
3. 如果umask的值为022,创建文件时指定的权限是775,则该文件的实际权限为 ( )
[A] 755 [B] 722 [C] 753 [D] 022
在Unix和类Unix系统中,
umask
是一个用于控制新创建文件和目录权限的遮罩值。当创建一个新文件或目录时,系统会使用你指定的权限(如775
对于文件或777
对于目录)与umask
值进行按位与的反操作(即AND NOT
或~AND
)来确定最终的权限。首先,我们来解释给定的权限和
umask
值:
- 文件指定的权限:
775
(二进制为111 111 101
)umask
的值:022
(二进制为000 010 010
)现在,我们使用按位与的反操作来确定最终权限:
- 对于用户(owner)权限:
111 AND NOT 000 = 111
(即7)- 对于组(group)权限:
111 AND NOT 010 = 101
(即5)- 对于其他用户(others)权限:
101 AND NOT 010 = 101
(即5)因此,文件的最终权限是
755
。【正确答案: [A] 755】
4. 用open( )创建新文件时,若该文件存在则可以返回错误信息的参数是 ( )
[A] O_CREAT [B] O_EXCL [C] O_TRUNC [D] O_NOCTTY
在Unix和类Unix系统(如Linux)的
open()
系统调用中,要创建新文件或打开已存在的文件,并希望在文件已存在时返回错误信息,你需要使用特定的标志组合。
- [A]
O_CREAT
:如果文件不存在,则创建它。如果文件已存在,则此选项不产生任何效果。- [B]
O_EXCL
:与O_CREAT
一起使用时,如果文件已存在,则调用失败并返回错误。- [C]
O_TRUNC
:如果文件存在且为普通文件,并且成功打开以进行写操作或读/写操作,则将其长度截断为零。此选项对打开已存在的文件有影响,但它不会阻止文件被打开。- [D]
O_NOCTTY
:如果路径名指向终端设备,则不要将该设备分配为进程的控制终端。这与文件是否已存在无关。因此,要在文件已存在时返回错误信息,你需要使用
O_CREAT
和O_EXCL
这两个标志的组合。但根据题目要求,单独的标志是O_EXCL
。【正确答案: [B] O_EXCL】
5. 如果键盘输入为abcdef,程序如下所示,打印结果应该是( )
char buffer[6];
……
fgets(buffer, 6, stdin);
printf(“%s”, buffer);
[A] abcde [B] abcdef [C] abcdef 后出现乱码 [D] 段错误
首先,我们需要理解
fgets
函数的工作原理。fgets
函数从指定的流(在这个例子中是stdin
)中读取一行,并将其存储在提供的缓冲区中,直到遇到换行符('\n')、EOF或达到指定的最大字符数(包括空字符'\0')。给定的代码片段中,
fgets(buffer, 6, stdin);
尝试从stdin
读取最多5个字符(因为缓冲区大小为6,但还需要一个位置来存储字符串的结束符'\0')并存储在buffer
数组中。现在,考虑键盘输入为
abcdef
:
fgets
会读取前5个字符('a', 'b', 'c', 'd', 'e')并存储在buffer
中。fgets
会在字符串的末尾添加一个空字符'\0'来标记字符串的结束。- 然后,
printf(“%s”, buffer);
会打印出存储在buffer
中的字符串,即abcde
。由于
fgets
在读取5个字符后遇到换行符('\n')之前就已经达到了最大字符数,所以换行符不会被读取到buffer
中,因此输出中也不会包含换行符或任何其他额外的字符(如选项C中的乱码)。另外,由于缓冲区足够大来存储输入的字符串和结束符,所以也不会出现段错误(如选项D)。
【正确答案: [A] abcde】
6. 下列哪个函数无法传递进程结束时的状态 ( )
[A]close [B] exit [C] _exit [D] return
首先,我们要理解这些函数在Unix和类Unix系统(如Linux)中的用途和上下文。
- [A]
close
:这个函数用于关闭一个文件描述符。它并不涉及进程的结束或状态传递。- [B]
exit
:这个函数用于终止调用它的进程,并返回一个状态值给父进程。父进程可以通过wait()
或waitpid()
等系统调用来获取这个状态值。- [C]
_exit
:这个函数也用于终止进程,但它不会执行标准I/O清理(如刷新缓冲区)。它也会返回一个状态值给父进程。- [D]
return
:在main函数中,return
语句用于结束程序并返回一个状态值给操作系统(实际上是传递给父进程)。在非main函数中,return
只返回给调用它的函数。现在,我们来分析这些选项:
- [A]
close
显然与进程结束时的状态传递无关。- [B]
exit
和 [C]_exit
都可以传递一个状态值给父进程。- [D]
return
在main函数中也可以传递一个状态值给操作系统(实际上是父进程)。但是,如果
return
在非main函数中使用,它就不会传递进程结束时的状态给父进程,因为它只是返回给调用它的函数。但题目没有明确说return
是在哪个函数中使用,所以我们假设它是在main函数中使用的。然而,从题目的语境来看,我们是在寻找一个“无法传递进程结束时的状态”的函数,而这个函数就是[A]
close
。【正确答案: [A]close】
7. 以下哪种用法可以等待接收进程号为pid的子进程的退出状态 ( )
[A] waitpid(pid, &status, 0) [B] waitpid(pid, &status, WNOHANG)
[C] waitpid(-1, &status, 0) [D] waitpid(-1, &status, WNOHANG)
在给出的选项中,要等待接收进程号为
pid
的子进程的退出状态,应该使用waitpid
函数并指定正确的参数。
- [A]
waitpid(pid, &status, 0)
:这个用法会阻塞父进程,直到进程ID为pid
的子进程结束,并将子进程的退出状态保存在status
指向的变量中。这是符合题目要求的。- [B]
waitpid(pid, &status, WNOHANG)
:这个用法使用WNOHANG
选项,它会使waitpid
在子进程没有结束时立即返回。这意味着父进程不会被阻塞,但可能无法立即获取子进程的退出状态。- [C]
waitpid(-1, &status, 0)
:这个用法会阻塞父进程,直到任意一个子进程结束。因为指定了-1
作为进程ID,所以它会等待任何子进程。- [D]
waitpid(-1, &status, WNOHANG)
:这个用法与[B]类似,但会等待任意子进程。同样,由于使用了WNOHANG
选项,父进程可能不会被阻塞,但也可能无法立即获取子进程的退出状态。因此,要等待接收进程号为
pid
的子进程的退出状态,应该使用[A]waitpid(pid, &status, 0)
。这个选项会阻塞父进程,直到指定的子进程结束,并获取其退出状态。【正确答案: [A] waitpid(pid, &status, 0)】
8. 函数waitpid的返回值等于0时表示的含义是 ( )
[A] 等待的子进程退出 [B] 使用选项WNOHANG且没有子进程退出
[C] 调用出错 [D] 不确定
函数
waitpid
的返回值等于0时,表示的含义是:[B] 使用选项WNOHANG且没有子进程退出。
当
waitpid
函数被调用时,如果设置了WNOHANG
选项(表示非阻塞模式),并且指定的子进程尚未退出,那么waitpid
会立即返回,并且返回值是0。这表示没有已退出的子进程可供收集。因此,选项[B]是正确的。选项[A]表示等待的子进程已经退出,此时
waitpid
的返回值应该是子进程的进程ID,而不是0。选项[C]表示调用出错,此时waitpid
的返回值是-1。选项[D]表示不确定,这是不正确的,因为waitpid
的返回值有明确的意义。【正确答案: [B] 使用选项WNOHANG且没有子进程退出】
9. 下列对无名管道描述错误的是 ( )
[A] 半双工的通信模式
[B] 有固定的读端和写端
[C] 可以使用lseek函数
[D] 只存在于内存中
对于无名管道的描述,逐一分析选项:
[A] 半双工的通信模式:这是正确的。无名管道是一种半双工的通信方式,数据在同一时刻只能在一个方向上流动。
[B] 有固定的读端和写端:这也是正确的。在无名管道中,fd[0]固定用于读管道,fd[1]固定用于写管道。
[C] 可以使用lseek函数:这是错误的。无名管道是基于管道的顺序流通信,只支持顺序读写操作,无法像文件一样进行随机访问。因此,它无法使用
lseek
函数进行随机访问。[D] 只存在于内存中:这是正确的。无名管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
【正确答案: [C] 可以使用lseek函数】
10.下列对于有名管道描述错误的是 ( )
[A] 可以用于互不相关的进程间
[B] 通过路径名来打开有名管道
[C] 在文件系统中可见
[D] 管道内容保存在磁盘上
对于有名管道(也称为命名管道或FIFO)的描述,分析选项:
[A] 可以用于互不相关的进程间:这是正确的。有名管道可以被不相关的进程用来进行通信,只要它们有对有名管道的访问权限。
[B] 通过路径名来打开有名管道:这也是正确的。有名管道在文件系统中有一个对应的路径名,可以通过
open
等系统调用来打开这个路径名以进行读写操作。[C] 在文件系统中可见:这是正确的。有名管道在文件系统中有一个对应的节点,可以通过
ls
等命令在文件系统中看到。[D] 管道内容保存在磁盘上:这是错误的。尽管有名管道在文件系统中有一个对应的节点,但其内容与普通的文件不同。有名管道的内容是保存在内核的缓冲区中的,而不是直接保存在磁盘上。有名管道的内容只在管道打开且进程之间进行通信时存在,当所有对有名管道的引用都关闭后,其内容将被丢弃。
【正确答案: [D] 管道内容保存在磁盘上】
11. 下列不属于用户进程对信号的响应方式的是 ( )
[A] 忽略信号 [B] 保存信号 [C] 捕捉信号 [D] 按缺省方式处理
在UNIX和Linux系统中,用户进程对信号的响应方式主要有三种,现在我们针对选项进行逐一分析:
- [A] 忽略信号:进程可以选择忽略某些信号,即不对信号做出任何响应。但需要注意的是,有两个信号(SIGKILL和SIGSTOP)是不能被忽略的。
- [B] 保存信号:这并不是用户进程对信号的响应方式之一。进程在接收到信号后,通常选择忽略、捕捉或按缺省方式处理,但不会直接“保存”信号。
- [C] 捕捉信号:进程可以定义自己的信号处理函数,当接收到某个信号时,就执行这个处理函数。这种方式允许进程对信号进行自定义的响应。
- [D] 按缺省方式处理:如果没有特殊指定,Linux系统会对每种信号都规定一个默认的操作。当进程接收到信号时,如果没有其他的处理方式,就会按照这种默认的操作来响应。
【正确答案: [B] 保存信号】
12. 不能被用户进程屏蔽的信号是 ( )
[A] SIGINT [B] SIGSTOP [C] SIGQUIT [D] SIGILL
在Linux系统中,信号屏蔽是指进程暂时阻塞某些信号的传递和处理。但并非所有的信号都可以被用户进程屏蔽。
- [A] SIGINT:这是一个中断信号,通常由用户按下Ctrl+C产生。这个信号可以被用户进程屏蔽。
- [B] SIGSTOP:这是一个停止信号,用于立即停止进程的执行。这个信号是不能被用户进程屏蔽的。
- [C] SIGQUIT:这是一个退出信号,通常由用户按下Ctrl+\产生。这个信号可以被用户进程屏蔽。
- [D] SIGILL:这是一个非法指令信号,表示进程执行了非法的指令。这个信号通常与硬件或程序错误相关,但也可以被用户进程屏蔽。
【正确答案: [B] SIGSTOP】
13. 默认情况下,不会终止进程的信号是 ( )
[A] SIGINT [B] SIGKILL [C] SIGALRM [D] SIGCHLD
对于给定的信号,我们分析它们默认的行为:
- [A] SIGINT:中断信号,通常由用户按下Ctrl+C产生。默认情况下,它会终止前台进程。
- [B] SIGKILL:终止信号,不能被进程捕获、阻塞或忽略。它总是立即终止进程。
- [C] SIGALRM:定时器信号,当定时器到期时发送。默认情况下,它不会终止进程,但进程可以捕获此信号并执行相应的操作。
- [D] SIGCHLD:子进程状态改变信号,当子进程终止、被停止或继续时发送。默认情况下,它不会终止进程。
因此,默认情况下,不会终止进程的信号是[C] SIGALRM和[D] SIGCHLD。但根据题目要求选择一个答案,通常SIGALRM更常被提及为与定时器相关的信号,而SIGCHLD则通常与进程管理相关,并不直接用于终止进程。所以,更准确的答案是[C] SIGALRM。
【正确答案: [C] SIGALRM 】
14. 下列不属于IPC对象的是 ( )
[A] 管道 [B] 共享内存 [C] 消息队列 [D] 信号灯
在进程间通信(IPC)的上下文中,我们考虑以下选项:
- [A] 管道(Pipe):管道是IPC的一种机制,它允许不同进程之间进行数据交换。因此,它是IPC对象。
- [B] 共享内存(Shared Memory):共享内存也是IPC的一种机制,允许进程共享同一块物理内存区域。因此,它也是IPC对象。
- [C] 消息队列(Message Queue):消息队列是操作系统提供的一种IPC机制,允许进程通过发送和接收消息来进行通信。所以,它也是IPC对象。
- [D] 信号灯(Signal Light):在IPC的上下文中,我们通常指的是信号量(Semaphore)而不是“信号灯”。信号量是一种用于控制对共享资源的访问的IPC机制。但“信号灯”并不是标准的IPC对象名称。
根据以上分析,不属于IPC对象的是[D] 信号灯(除非这里的“信号灯”是指信号量的误写或误解)。但基于标准的IPC术语,正确的答案应该是[D],因为它不是IPC的标准对象名称。如果“信号灯”实际上是指信号量(Semaphore),那么所有选项都是IPC对象。但在没有进一步澄清的情况下,我们应该选择[D] 信号灯。
【正确答案: [D] 信号灯】
15. 下列哪种机制可以用于线程之间的同步 ( )
[A] 信号 [B] IPC信号灯
[C] POSIX有名信号量 [D] POSIX无名信号量
在给出的选项中,用于线程之间同步的机制有:
- [D] POSIX无名信号量:这是一种在多线程和多进程编程中用于同步和互斥的机制。它是POSIX标准中定义的一组函数和数据结构,用于实现线程或进程之间的同步。信号量是一个计数器,用于控制对共享资源的访问。它可以用来解决多个线程或进程之间的竞争条件和互斥访问问题。
对于其他选项:
- [A] 信号:在操作系统中,信号主要用于进程间的通信,而不是线程间的同步。
- [B] IPC信号灯:这个描述可能有些模糊,但通常IPC中的“信号灯”指的是信号量(Semaphore),而不是一个独立的同步机制。
- [C] POSIX有名信号量:虽然有名信号量也可以用于同步,但在这个问题中,它并不是唯一或首选的答案,因为无名信号量在多线程同步中更为常见和方便。
【正确答案: [D] POSIX无名信号量】
二、判断题(每题1分,总分15分)
1.Linux下进程的模式分为用户态,内核态和系统态 ( )
在Linux系统中,进程的运行模式主要分为用户态(user mode)和内核态(kernel mode)。
- 用户态(user mode):当进程执行用户自己的代码时,该进程处于用户态。在用户态下,进程可以直接读取用户程序的数据,但此时CPU访问系统资源的权限是受限的。
- 内核态(kernel mode):当进程执行系统内核代码时,该进程处于内核态。在内核态下,进程或程序几乎可以访问计算机的任何资源,不受限制。此时,CPU可以访问计算机的所有资源。
【正确答案:×】
2.每个进程的进程号和父进程号在进程执行期间不会改变 ( )
在Linux和大多数Unix-like系统中,每个进程都有一个唯一的进程ID(PID)和一个父进程ID(PPID)。这些ID在进程创建时由内核分配,并且在进程的生命周期内保持不变。即使进程执行了exec系统调用(这会导致进程加载并执行新的程序),其PID和PPID也不会改变。只有当进程结束时,它的PID才会被释放,以便后续创建的进程可以使用。
【正确答案:✓】
3.子进程被创建后从fork()的下一条语句开始执行 ( )
在Unix和类Unix系统(如Linux)中,当调用
fork()
函数时,会创建一个与父进程几乎完全相同的子进程。这个“几乎完全相同”的意思包括子进程会获得父进程当前执行点的复制品,也就是说,子进程会从fork()
系统调用的下一条语句开始执行。同时,父进程会继续执行
fork()
之后的语句,而子进程则执行它自己的副本中的相同代码部分。这样,父子进程就有了各自独立的执行路径,但它们都起源于fork()
调用。【正确答案:✓】
4.子进程的进程号等于父进程的进程号加1 ( )
这个说法是不正确的。在Linux和其他Unix-like系统中,子进程的进程ID(PID)是由系统内核在创建子进程时分配的,它并不简单地等于父进程的PID加1。实际上,PID是一个唯一的、非负的整数,用于标识系统中的每个进程。
当父进程使用
fork()
系统调用创建子进程时,内核会为新进程分配一个新的、唯一的PID。这个PID的分配过程通常是由操作系统内核的调度器来管理的,而不是简单地基于父进程的PID来生成。因此,子进程的PID可能与父进程的PID有任何关系,它们只是系统中的一个独立标识符。【正确答案:×】
5.执行_exit()函数时不会清理IO缓冲 ( )
_exit()是一个系统调用函数,用于直接使进程停止运行,并清除其使用的内存空间,以及销毁在内核中的各种数据结构。然而,与exit()函数不同,_exit()不会刷新或清理标准I/O缓冲区。这意味着,如果程序在调用_exit()之前向标准输出(例如printf函数使用的缓冲区)写入了数据,但这些数据还没有被刷新到实际的文件或设备中,那么这些数据将会丢失,因为_exit()不会执行任何刷新缓冲区的操作。
相比之下,exit()函数是一个C语言标准库函数,它在调用系统调用_exit()之前会先执行一些清理工作,包括刷新标准I/O缓冲区。因此,使用exit()函数可以确保在程序终止之前,所有未写出的数据都被正确地刷新到输出设备。
【正确答案:✓】
6.exec函数族可以创建一个新的进程来执行指定的程序 ( )
exec
函数族(如execlp()
,execv()
,execle()
,execve()
,execlp()
,execvp()
等)并不创建一个新的进程来执行指定的程序,而是用指定的程序来替换当前进程的内存映像。换句话说,调用exec
函数后,当前进程的代码、数据、堆和栈都会被新的程序替换,然后从新的程序的入口点开始执行。因此,从某种角度看,调用exec
后原进程已经“消失”了,其PID并没有改变,但执行的是全新的程序。要创建一个新的进程来执行指定的程序,应该首先使用
fork()
创建一个子进程,然后在子进程中调用exec()
来执行新的程序。这样,父进程和子进程就可以并行执行不同的任务了。【正确答案:×】
7.wait函数无法接收子进程退出的状态 ( )
wait函数是一个系统调用,用于使父进程等待子进程完成执行,并可以接收子进程退出的状态。在使用wait函数之前,必须先创建子进程。wait函数包含在unistd.h头文件中,其原型为:
pid_t wait(int*status);
。其中,status
是一个指向整型的指针,用于保存子进程的终止状态。当wait函数成功时,它会返回子进程的进程ID,同时可以通过status
参数获取子进程的退出状态。因此,wait函数不仅可以使父进程等待子进程的结束,还可以获取子进程的退出状态,是进程间同步和通信的重要工具。
【正确答案:×】
8.无名管道只能用于具有亲缘关系的进程间通信 ( )
无名管道(也称为管道)是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。通常,一个管道是由一个进程创建,然后该进程调用fork创建一个子进程,这个子进程会继承父进程打开的管道文件描述符,从而实现进程间的通信。由于无名管道是通过文件系统上的节点来实现通信的,因此它只能用于具有亲缘关系的进程之间。
与此相对,命名管道(也称为FIFO)则可以在不相关的进程之间使用,因为它有一个路径名与之关联,使得任何进程都可以访问它。
【正确答案:✓】
9.对命名管道的读写严格遵循先进先出的规则 ( )
命名管道,也被称为FIFO(first-in, first-out),在读写数据时严格遵循先进先出的规则。具体来说,写入FIFO的数据会被添加在队列的末尾,而读取FIFO的数据则会从队列的开头返回。这种机制确保了数据的顺序性和一致性,使得命名管道成为一种可靠的进程间通信方式。
【正确答案:✓】
10.信号既可以发给前台进程也可以发给后台进程 ( )
在操作系统中,信号是进程之间事件异步通知的一种方式,属于软中断。无论是前台进程还是后台进程,都可以接收信号。例如,在Linux中,ctrl+c产生的SIGINT信号就可以发送给前台进程,也可以发送给在后台运行的进程(如果进程支持接收该信号的话)。当然,前台进程和后台进程在接收信号和处理信号的方式上可能有所不同,但这并不影响它们都可以接收信号的事实。
【正确答案:✓】
11.可以用signal()向指定的进程发信号 ( )
在Unix和类Unix系统中,
signal()
函数通常用于设置信号处理函数,而不是直接用于向指定的进程发送信号。signal()
函数用于改变进程接收到特定信号时的行为。你可以使用它来指定一个函数,当进程接收到某个信号时,该函数会被调用。要向指定的进程发送信号,你应该使用
kill()
函数或raise()
函数。kill()
函数允许你向一个指定的进程ID(PID)发送信号,而raise()
函数则用于向当前进程发送信号。因此,
signal()
函数本身并不用于发送信号,而是用于设置信号的处理方式。【正确答案:×】
12.无法用信号实现进程间的同步 ( )
虽然信号(signal)主要用于处理异步事件,如中断、终止等,但在某些情况下,它们也可以用于实现进程间的同步。
虽然信号不是专门为进程同步设计的,但你可以通过精心设计和使用信号来实现进程间的同步。例如,一个进程可以发送一个特定的信号来通知另一个进程某个事件已经发生,而接收信号的进程可以据此进行相应的同步操作。
然而,需要注意的是,由于信号的处理是异步的,因此使用信号来实现进程同步可能会比使用其他同步机制(如互斥锁、条件变量、信号量等)更加复杂和难以控制。因此,在实际应用中,通常会优先考虑使用其他更适合进程同步的机制。
【正确答案:×】
13.消息队列可以按照消息类型读取消息 ( )
消息队列(Message Queue)是一种在分布式系统中进行异步通信的机制,它允许生产者将消息发送到队列中,然后由消费者从队列中接收并处理这些消息。消息队列的一个重要特性是它可以支持按照消息类型来读取消息。
这意味着消费者不需要按照消息进入队列的顺序(即先进先出,FIFO)来读取消息,而是可以根据自定义的条件或消息的类型来选择性地读取和处理消息。这种灵活性使得消息队列在处理复杂业务逻辑和异步通信场景时非常有用。
【正确答案:✓】
14.消息队列的读写只能采用阻塞的方式 ( )
消息队列的读写并不只能采用阻塞的方式。
在消息队列中,有阻塞队列和非阻塞队列之分。阻塞队列在队列为空时,读取操作会被阻塞,直到有消息到来;当队列满时,写入操作也会被阻塞,直到队列中有空位。而非阻塞队列则不会阻塞,当队列为空时,读取操作会立即返回(可能是null或表示无消息的特定值),当队列满时,写入操作可能会失败或丢弃旧消息。
因此,消息队列的读写可以采用阻塞方式,也可以采用非阻塞方式,这取决于具体的应用场景和需求。
【正确答案:×】
15.共享内存是一种最为高效的进程间通信方式 ( )
需要注意的是“最为高效”这一表述可能需要根据具体的应用场景和上下文来理解。
共享内存允许两个或多个进程访问同一块内存空间,从而实现进程间的数据共享和通信。由于数据不需要在进程间复制,因此共享内存通常被认为是进程间通信(IPC)中最快的方式之一。它避免了内核和用户空间之间的数据拷贝,从而减少了通信开销。
然而,共享内存也带来了一些挑战,如同步和互斥问题。当多个进程同时访问共享内存时,需要采取适当的同步机制来确保数据的一致性和完整性。此外,共享内存的管理也相对复杂,需要谨慎处理内存分配、释放和错误处理等问题。
因此,虽然共享内存通常被认为是一种高效的进程间通信方式,但在具体使用时还需要根据应用场景和需求进行权衡和选择。
【正确答案:✓】
三、简答题(总分30分)
1.请描述进程和程序的区别 (6分)
主要区别如下:
- 动态与静态:
- 程序(Program):是一个静态的概念,指的是一组计算机能识别和执行的指令集合,通常以某种程序设计语言编写,并存储于某种介质上。它描述了计算机应如何执行特定的任务,但并不直接涉及执行。
- 进程(Process):是程序的一次执行过程,是动态的概念。当程序被加载到内存中并由操作系统调度执行时,它就被称为一个进程。进程是系统进行资源分配和调度的基本单位。
- 生命周期:
- 程序:一旦编写完成并存储于介质上,其存在就是永久的,除非被删除或修改。
- 进程:其生命周期是有限的。它因创建而产生,因调度而执行,可能因得不到资源而暂停,最终因完成执行或错误而消亡。
- 组成结构:
- 程序:由指令、数据及其组织形式组成,它本身不包含执行时的系统状态信息。
- 进程:由程序、数据和进程控制块(PCB)组成。PCB包含了系统用于描述进程状态和控制进程运行所需的信息,如程序计数器、CPU寄存器、进程状态等。
- 独立性:
- 程序:作为静态的指令集合,不具有独立性。
- 进程:作为独立运行的实体,它拥有独立的地址空间、系统资源和执行环境,是系统进行资源分配和调度的独立单位。
- 并发性:
- 程序:本身并不具有并发性,它是顺序执行的指令集合。
- 进程:在操作系统中,多个进程可以并发执行,即在同一时间段内,多个进程在系统中交替或重叠执行。
- 执行结果:
- 程序:在没有被执行时,不产生任何结果。
- 进程:通过执行程序,可以产生结果,如输出数据、改变系统状态等。
综上所述,进程和程序的主要区别在于它们的动态与静态性质、生命周期、组成结构、独立性、并发性和执行结果等方面。
2.指出静态库和共享库的区别(使用方法,对程序的影响) (8分)
静态库和共享库在使用方法和对程序的影响上存在一些显著的差异。
- 使用方法:
- 静态库:在静态链接时使用,每个程序都有一份静态库的副本。这意味着当程序被编译时,静态库中的代码和数据会被直接复制到最终的可执行文件中。因此,使用静态库的程序在运行时不再需要静态库的存在。
- 共享库:在动态链接时使用,多个程序可以共享同一个共享库。当程序被编译时,它并不直接包含共享库中的代码和数据,而是引用共享库中的函数和数据。在程序运行时,操作系统会负责将共享库加载到内存中,并将程序中的引用解析为共享库中的实际地址。
- 对程序的影响:
- 静态库:
- 优点:静态库的使用相对简单,因为它在编译时就已经将所需的代码和数据复制到可执行文件中,所以程序在运行时不再需要依赖外部库。此外,由于静态库中的代码和数据是嵌入到可执行文件中的,所以它可以提供更好的性能,因为避免了运行时动态链接的开销。
- 缺点:静态库的一个主要缺点是它会显著增加可执行文件的大小。因为每个使用静态库的程序都需要包含一份完整的库副本,所以即使多个程序使用了相同的库函数,它们也会分别包含一份完整的库代码和数据。这会导致磁盘空间的浪费,并在多进程操作系统下浪费内存。另外,如果静态库更新了,所有使用它的应用程序都需要重新编译和发布。
- 共享库:
- 优点:共享库的主要优点是它可以显著减少磁盘空间和内存的使用。因为多个程序可以共享同一个共享库,所以它们不再需要各自包含一份完整的库副本。此外,共享库还使得程序更新变得更加容易。当共享库更新时,只需要重新编译和发布共享库本身,而不需要重新编译和发布所有使用它的程序。
- 缺点:共享库的一个主要缺点是它可能会增加程序的复杂性。因为程序在运行时需要动态地链接到共享库,所以如果共享库中的函数或数据结构发生了更改,那么所有使用它的程序都需要进行相应的修改和重新编译。此外,如果多个程序同时访问同一个共享库,并且该库中的数据是可变的,那么就需要采取适当的同步措施来避免数据竞争和不一致性的问题。
3.写出设置信号处理函数signal的原型 (6分)
在Unix和类Unix系统中,
signal
函数用于设置进程接收到特定信号时的行为。它的原型在<signal.h>
头文件中定义,如下所示:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
其中:
signum
是要捕获的信号的编号(例如,SIGINT
、SIGTERM
等)。handler
是当接收到信号时要调用的函数的指针。这个函数通常被称为信号处理程序(signal handler)。如果handler
被设置为SIG_IGN
,则忽略该信号;如果设置为SIG_DFL
,则恢复为系统默认的信号处理方式。
signal
函数返回一个指向先前关联于signum
的信号的信号处理函数的指针。如果出现错误,则返回SIG_ERR
。需要注意的是,尽管
signal
函数在POSIX.1-1990标准中被定义,但由于它在处理多个信号时的语义在某些系统上可能不是可重入的(reentrant),因此在编写多线程程序或处理多个信号时,通常建议使用sigaction
函数,它提供了更强大和更可靠的信号处理能力。
4.程序代码如下,请按执行顺序写出输出结果 (6分)
int main()
{
pid_t pid1,pid2;
if((pid1=fork()) = = 0)
{
sleep(3);
printf(“info1 from child process_1\n”);
exit(0);
printf(“info2 from child process_1\n”);
}
else{
if((pid2=fork()) = = 0)
{
sleep(1);
printf(“info1 from child process_2\n”);
exit(0);
}
else
{
wait(NULL);
wait(NULL);
printf(“info1 from parent process\n”);
printf(“info2 from parent process”);
_exit(0);
}
}
首先,让我们纠正代码中的一个小错误。在
if
语句中,你应该使用==
而不是= =
来比较两个值是否相等。现在,我们按照执行顺序来分析输出。代码分析:
主进程首先执行
fork()
,创建一个子进程pid1
。
- 如果
pid1
为0,则当前进程是子进程pid1
。- 如果
pid1
非0,则当前进程是父进程。对于子进程
pid1
:
- 调用
sleep(3);
暂停3秒。- 打印
info1 from child process_1\n
。- 调用
exit(0);
终止进程,所以后面的printf
不会被执行。对于父进程:
调用另一个
fork()
创建另一个子进程pid2
。
- 如果
pid2
为0,则当前进程是子进程pid2
。
- 调用
sleep(1);
暂停1秒。- 打印
info1 from child process_2\n
。- 调用
exit(0);
终止进程。- 如果
pid2
非0,则当前进程仍然是父进程。
- 调用
wait(NULL);
等待第一个子进程(pid1
)结束。- 调用
wait(NULL);
等待第二个子进程(pid2
)结束(尽管此时它可能已经结束了)。- 打印
info1 from parent process\n
。- 打印
info2 from parent process
。- 调用
_exit(0);
终止进程。输出结果(按执行顺序):
- 由于
pid2
的子进程先暂停1秒,所以它的printf
会在pid1
的子进程的printf
之前执行(尽管父进程先创建了pid1
的子进程)。- 父进程会等待两个子进程都结束后才打印它的信息。
因此,输出可能是:
复制代码
info1 from child process_2
info1 from child process_1
info1 from parent process
info2 from parent process
注意:尽管两个子进程的输出顺序在逻辑上是确定的(因为
pid2
的子进程先暂停1秒),但在实际执行中,由于调度和上下文切换的不确定性,这两个输出之间可能几乎没有间隔,导致看起来像是同时发生的。但按照上述逻辑分析,pid2
的子进程的输出会先出现。
5.列出任意四种进程间通信的方式(4分)
进程间通信(IPC)是操作系统中不同进程之间传递信息或数据的一种机制。以下是四种常见的进程间通信方式:
- 管道(Pipe):
- 管道是最早出现的IPC形式之一,通常指无名管道(匿名管道)。
- 它只能用于具有亲缘关系的进程之间的通信,例如父子进程之间。
- 管道是半双工的,数据只能单向流动。
- 有名管道(FIFO)允许无亲缘关系的进程间通信。
- 消息队列(Message Queue):
- 消息队列是消息的链接表,存放在内核中。
- 它克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 消息队列允许进程以消息的格式(具有特定的格式和属性)来发送和接收数据。
- 有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。
- 共享内存(Shared Memory):
- 共享内存允许多个进程访问同一块内存空间。
- 它是所有IPC方式中最快的一种,因为数据不需要在不同的进程间复制。
- 但由于多个进程可以同时操作共享内存,所以需要进行同步以避免数据冲突。
- 信号(Signal):
- 信号是一种异步的通知机制,用于通知进程有某种事件发生。
- 除了用于进程间通信外,进程还可以发送信号给进程本身。
- 信号量(Semaphore)是一种特殊的信号,它主要用于控制多个进程对共享资源的访问,以实现进程间的同步和互斥。
四、问答题(25分)
1.指出创建守护进程的步骤(5分)
创建守护进程的步骤主要包括以下几个方面:
- 创建子进程,并退出父进程:这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此首先创建子进程,并终止父进程,使得程序在Shell终端里造成一个已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他的命令,从而使得程序以孤儿进程形式运行,在形式上做到了与控制终端的脱离。
- 在子进程中创建新会话:这是创建守护进程中最重要的一步。可以使用系统函数
setsid
来创建一个新的会话,并担任该会话组的组长。setsid
函数的作用是使进程摆脱原会话的控制、摆脱原进程组的控制以及摆脱原控制终端的控制。- 改变工作目录:使用
fork
创建的子进程会继承父进程的当前工作目录。为了避免在进程运行过程中,当前目录所在的文件系统被卸载,通常会将当前工作目录换成其他的路径,如“/”或“/tmp”等。这可以通过chdir
函数来实现。- 重设文件创建掩码:文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用
fork
函数新建的子进程继承了父进程的文件创建掩码,这可能会给该子进程使用文件带来麻烦。因此,通常会将文件创建掩码设置为0。- 关闭其他文件描述符:关闭不再需要的文件描述符,以避免潜在的资源泄露问题。
完成以上步骤后,守护进程就已经成功创建了。守护进程会在系统后台运行,并且不受任何终端的控制,用于执行特定的系统任务。很多守护进程在系统引导时启动,并且一直运行直到系统关闭。
2.请画出Linux中进程的状态切换图(5分)
Linux中进程的状态切换图如下:
graph LR
新建(New) --> 就绪(Ready)
就绪(Ready) --> 运行(Running)
运行(Running) --> 等待(Waiting)
等待(Waiting) --> 就绪(Ready)
运行(Running) --> 终止(Terminated)解释:
- 新建(New):进程刚刚创建,尚未进入就绪队列。
- 就绪(Ready):进程已经准备好运行,但尚未获得CPU时间片。
- 运行(Running):进程正在执行,占用CPU资源。
- 等待(Waiting):进程因等待某个事件(如I/O操作完成)而暂停执行。
- 终止(Terminated):进程已经完成执行或被强制终止。
3.用标准IO的fgets函数计算一个文件的行数(写核心代码即可)(5分)
+-----------+
| New |
+-----+---+
|
V
+-----------+
| Ready | <-------\
+-----+---+ | (on resource availability)
| |
V |
+-----------+ |
| Running | -----> \ (time slice expires, preemption, etc.)
+-----+---+ |
| |
V |
+-----------+ |
| Blocked | <-----/ (on I/O completion, event, etc.)
+-----------+
|
V
+-----------+
| Terminated|
+-----------+在Linux中,进程的状态转换图通常包括多个状态,这些状态描述了进程在其生命周期中的不同阶段。以下是一个简化的Linux进程状态切换图的描述,以及一个基于该描述的简化图形表示:
进程状态:
- 新建状态(New):进程刚刚被创建,但尚未进入就绪队列。
- 就绪状态(Ready):进程已准备好运行,等待被调度到CPU上执行。
- 运行状态(Running):进程正在CPU上执行。
- 阻塞状态(Blocked/Waiting):进程因等待某个事件(如I/O操作、资源可用性等)而无法继续执行。
- 终止状态(Terminated/Exit):进程执行完毕或由于某种原因被终止。
注意:
- 从新建状态(New)到就绪状态(Ready)的转换发生在进程创建完成后,系统为其分配了必要的资源,并将其放入就绪队列中。
- 从就绪状态(Ready)到运行状态(Running)的转换由进程调度器完成,当CPU空闲时,调度器会从就绪队列中选择一个进程来执行。
- 运行状态(Running)到阻塞状态(Blocked)的转换可能由于多种原因触发,如进程等待I/O操作完成、等待某个信号、等待资源等。
- 阻塞状态(Blocked)到就绪状态(Ready)的转换发生在进程等待的事件发生时,如I/O操作完成、信号接收、资源可用等。
- 运行状态(Running)到终止状态(Terminated)的转换发生在进程执行完毕或由于某种原因被终止时。
此外,虽然上面的状态切换图没有包含“可中断睡眠状态”和“不可中断睡眠状态”等更细分的状态,但这些状态可以视为阻塞状态(Blocked)的特殊情况,其中进程在等待特定事件时可能具有不同的响应行为。同样,“暂停状态”也可以被视为一种特殊的阻塞状态,其中进程被显式暂停执行。
4.编写程序实现如下功能(10分)
reader.c 从argv[1]所指定的文件中读取内容,依次写到管道/home/linux/myfifo中
writer.c 从管道/home/linux/myfifo中读取内容,写到argv[1]所指定的文件中并保存
代码中可省略头文件,/home/linux/myfifo无需创建
reader.c
int main(int argc, char *argv[]) {
int fd;
char buffer[1024];
ssize_t bytesRead;if (argc != 2) {
printf("Usage: %s <filename>\n", argv[0]);
return 1;
}fd = open(argv[1], O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}int fifo = open("/home/linux/myfifo", O_WRONLY);
if (fifo == -1) {
perror("open");
close(fd);
return 1;
}while ((bytesRead = read(fd, buffer, sizeof(buffer))) > 0) {
write(fifo, buffer, bytesRead);
}close(fd);
close(fifo);return 0;
}writer.c
int main(int argc, char *argv[]) {
int fd;
char buffer[1024];
ssize_t bytesRead;if (argc != 2) {
printf("Usage: %s <filename>\n", argv[0]);
return 1;
}int fifo = open("/home/linux/myfifo", O_RDONLY);
if (fifo == -1) {
perror("open");
return 1;
}fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
close(fifo);
return 1;
}while ((bytesRead = read(fifo, buffer, sizeof(buffer))) > 0) {
write(fd, buffer, bytesRead);
}close(fd);
close(fifo);return 0;
}