Linux系统编程第三章学习笔记
前言
本章讨论了Unix/Linux 中的进程管理;阐述了多任务处理原则;介绍了进程概念;并以一个编程示例来说明多任务处理、上下文切换和进程处理的各种原则和方法。多任务处理系统支持动态进程创建、进程终止,以及通过休眠与唤醒实现进程同步、进程关系,以及以二叉树的形式实现进程家族树,从而允许父进程等待子进程终止;提供了一个具体示例来阐释进程管理函数在操作系统内核中是如何工作的;然后,解释了Unix/Linux中各进程的来源,包括系统启动期间的初始进程、INIT进程、守护进程、登录进程以及可供用户执行命令的sh进程;接着,对进程的执行模式进行了讲解,以及如何通过中断、异常和系统调用从用户模式转换到内核模式;再接着,描述了用于进程管理的Unix/Linux系统调用,包括fork、wait、 exec 和exit;闸明了父进程与子进程之间的关系,包括进程终止和父进程等待操作之间关系的详细描述:解释了如何通过INIT进程处理孤儿进程,包括当前Linux中的subreaper进程,并通过示例演示了subreaper 进程;接着,详细介绍了如何通过exec更改进程执行映像,包括exeeve系统调用、命令行参数和环境变量;解释了I/O 重定向和管道的原则及方法,并通过示例展示了管道编程的方法:读者可借助本章的编程项目整合进程管理的各种概念和方法,实现用于执行命令的sh模拟器。sh模拟器的功能与标准sh完全相同。它支持简单命令、具有1O重定向的命令和通过管道连接的多个命令的执行。
1.多任务处理
多任务处理是指计算机系统同时处理多个独立任务的能力,可以通过多进程、多线程和时间片轮转等方式实现。在单处理器系统中,每次只能执行一个任务,通过上下文切换机制实现任务的快速切换。在多处理器系统中,不同CPU或处理器内核可以实时并行执行多项任务。多任务处理是所有操作系统和并行编程的基础。
2.进程的概念
操作系统是一个多任务处理系统
进程的定义:进程是对映像的执行
操作系统内核将并发执行视为使用系统资源的单个实体,包括内存空间、I/O设备和CPU时间。每个进程都由一个独特的数据结构表表示,称为进程控制块(PCB)或任务控制块(TCB),这里称为PROC结构体。PROC结构体包含了进程的所有信息,实际操作系统中可能包含许多字段。为了简单起见,我们定义了一个简单的PROC结构体来表示进程。
typedef struct proc{
struct proc *next;
int *ksp;
int pid;
int ppid;
int status;
int priority;
int kstack[1024];
} PROC;
- next是指向下一个PROC结构体的指针,用于在各种动态数据结构(如链表和队列)中维护PROC结构体。
- ksp字段是保存的堆栈指针,当某进程放弃使用CPU时,它会执行上下文保存在堆栈中,并将堆栈指针保存在PROC.ksp中,以便以后恢复。
- pid是识别一个进程的ID编号.
- ppid是父进程ID编号。
- status是进程的当前状态。
- priority是进程调度优先级。
- kstack是进程执行时的堆栈。
操作系统内核通常会在其数据区中定义有限数量的PROC结构体,表示为:
PROC proc[NPROC];
用来表示系统中的进程。在一个单CPU系统中,一次只能执行一个进程。操作系统内核通常会使用一个全局变量PROC指针来指向当前正在执行的进程。在多处理器系统中,有多个CPU可以实时并行执行多个进程,因此正在运行的进程可能是一个指针数组,每个指针指向在特定CPU上运行的进程。为简便起见,我们只考虑单CPU系统。
3.Unix/Linux中的进程
1.进程的来源
操作系统启动时,内核的启动代码会强制创建一个PID为0的初始进程。这通过分配一个PROC结构体(通常是proc[0])并初始化其内容来实现,然后将运行指针指向proc[0]。然后,系统执行初始进程P0。大多数操作系统都以这种方式开始运行第一个进程。
P0继续初始化系统,包括系统硬件和内核数据结构。然后,它挂载一个根文件系统,使系统能够访问文件。在初始化系统之后,P0复制一个子进程P1,并将进程切换到用户模式下运行P1。
2.INIT和守护进程
当进程P1开始运行时,它将其执行映像更改为INIT程序。因此,P1通常被称为INIT进程,因为它的执行映像是init 程序。P1 开始复刻出许多子进程。P1 的大部分子进程都是用来提供系统服务的。它们在后台运行,不与任何用户交互。这样的进程称为守护进程。守护进程的例子有
byslogd: 1og daemon process
inetd: Internet service daemon process
httpd: HTTP server daemon process
etc.
3.登录进程
P1除了守护进程外,还会复制许多LOGIN进程,每个终端上一个,用于用户登录。每个LOGIN进程打开三个与自己的终端相关联的文件流,即stdin(标准输入),stdout(标准输出)和stderr(标准错误)。每个文件流是指向进程堆区中FILE结构体的指针,每个FILE结构体记录一个文件描述符(数字),stdin的文件描述符是0,stdout的是1,stderr 的是2。
然后,每个LOGIN进程向stdout显示"login:",等待用户登录。用户账户信息保存在/etc/passwd和/etc/shadow文件中。每个用户账户在/etc/passwd文件中都有一行对应的记录,格式为:name;x;gid:uid;description:home:program。其中,name为用户登录名,x表示登录时检查密码,gid为用户组ID,uid为用户ID,home为用户主目录,program为用户登录后执行的初始程序。其他用户信息保存在/etc/shadow文件中。shadow文件的每一行都包含加密的用户密码,后面是可选的过期限制信息,如过期日期和时间等。
当用户尝试使用登录名和密码进行登录时,Linux将检查/etc/passwd文件和/etc/shadow文件,以验证用户的身份。
4.sh进程
成功登录后,LOGIN进程获取用户的gid和uid,并成为用户进程。它将目录更改为用户的主目录并执行列出的程序,通常是命令解释程序sh。用户进程通常称为sh进程,提示用户执行命令。sh进程自己直接执行一些特殊命令,如cd(更改目录)、exit(退出)、logout(注销)等。
大多数命令是各种bin目录(如/bin、/sbin、/usr/bin、/usr/local/bin等)中的可执行文件。对于每个(可执行文件)命令,sh会复刻一个子进程,并等待子进程终止。子进程将其执行映像更改为命令文件并执行命令程序。子进程在终止时会唤醒父进程sh,父进程将收集子进程终止状态,释放子进程的PROC结构体并提示执行另一个命令等。
除了简单的命令之外,sh还支持I/O重定向和通过管道连接的多个命令。
5.进程的执行模式
在Unix/Linux中,进程以两种不同的模式执行,即内核模式(Kmode)和用户模式(Umode)。
Umode进程只能通过一下三种方式进入Kmode:
(1)中断:外部设备发送给CPU信号,请求CPU服务。当在Umode下执行时,CPU中断是启用的。在发生中断时,CPU进入Kmode处理中断。
(2)陷阱:陷阱是错误条件,例如无效地址、非法指令、除以0等。CPU进入Kmode来处理错误。
(3)系统调用(syscall):是一种允许Umode进程进入Kmode以执行内核函数的机制。当某进程执行完内核函数后,它将期望结果和一个返回值返回Umode,该值通常为0(成功),-1(错误)。
4.进程的系统调用
Linux与进程管理相关的系统调用有如下几个:
fork(),wait(),exec(),exit()
详情可以使用指令man fork
等系列指令查看其详情
详情可参考Linux系统调用——进程控制(C/C++)(Linux)
5.I/O重定向
I/O重定向有三种形式:
标准输入重定向(<):使用"<"符号将一个文件或设备作为命令执行的输入,例如"command < input_file"。
标准输出重定向(>):使用">"符号将命令执行的结果输出到一个指定文件或设备,例如"command > output_file"。
标准错误重定向(2>):使用"2>"符号将命令执行的错误信息输出到一个指定文件或设备,例如"command 2> error_file"。可以使用"&>"符号将标准输出和错误信息输出到同一个文件或设备,例如"command &> output_and_error_file"。
详情可参考Linux 下强大得不得了的 IO 重定向
6.管道
管道是单向进程间通信通道,有读取端和写入端。读取端从写入端读取数据。管道有普通管道和命名管道。读写管道是同步、阻塞操作,可以支持非阻塞、异步读写操作。管道被视为一组相关进程间的有限尺寸FIFO通信通道。读写进程同步方式如下:读进程从管道上读取数据时,如果有数据,则读取并返回读取的字节数;如果没有数据但有写进程,则等待数据;如果没有数据也没有写进程,则返回0。写进程写入管道时,如果有空间,则尽可能多地写入,直到管道写满;如果没有空间但有读进程,则等待空间;如果没有空间也没有读进程,则必须将这种情况视为管道中断错误并中止写入。
管道编程实践
详情可参考Linux的管道pipe实现原理
苏格拉底挑战
Linux多任务处理知识学习
当然,我会以一系列问题的方式引导你学习Linux多任务处理的知识。让我们开始:
- 你是否了解Linux操作系统中的进程是什么?如果了解,请简要描述一下。
进程在Linux中由进程标识符(PID)唯一标识,每个进程都有一个父进程,而且可以创建一个或多个子进程。进程可以执行各种任务,从简单的系统任务到复杂的应用程序。进程之间可以通过进程间通信(IPC)机制进行数据交换,如管道、套接字、信号等。
Linux操作系统使用进程管理来调度和协调不同进程的执行,以确保系统资源的合理分配和高效利用。进程管理是Linux的核心功能之一,它使多任务处理和多用户环境成为可能,为用户提供了强大的灵活性和控制权。
非常好的解释!现在,让我们继续深入了解Linux多任务处理:
- 你知道Linux中的进程调度是如何工作的吗?描述一下Linux是如何决定哪个进程执行以及多久的。
以下是Linux进程调度的基本工作原理:
多级反馈队列:Linux维护了多个就绪队列,每个队列具有不同的优先级。通常,有高、中、低三个级别,每个队列都包含了就绪状态的进程。新创建的进程通常被插入到最高优先级队列。
时间片(Time Slice):每个队列都有一个特定的时间片大小,这是进程在CPU上执行的时间量。较高优先级队列的时间片较短,而较低优先级队列的时间片较长。时间片的目的是确保每个进程都有机会在CPU上执行,而不会永远占用它。
调度算法:Linux使用基于优先级的调度算法来确定哪个进程应该执行。在同一优先级队列中,按照先进先出(FIFO)的顺序选择下一个进程。如果一个进程使用完了其时间片,它将被移到下一个较低优先级队列中,从而为更低优先级的进程让出CPU。
抢占:Linux支持抢占式调度,这意味着高优先级的进程可以在低优先级进程的时间片内抢占CPU。这有助于确保重要任务能够及时执行。
动态优先级调整:Linux还可以根据进程的行为进行动态优先级调整,以便更好地响应不同进程的需求。例如,I/O密集型进程可能会在等待I/O操作完成时被提高优先级,以减少等待时间。
你对Linux进程调度的解释非常详细,理解得很透彻。接下来的问题是:
- 在Linux中,进程如何创建和终止?请简要描述这两个过程。
进程创建:
fork()系统调用: 进程的创建通常以fork()系统调用开始。当一个进程调用fork()时,操作系统会创建一个与调用进程几乎完全相同的新进程,包括相同的代码、数据、文件描述符等。这个新进程称为子进程。
子进程的修改: 通常,在fork()之后,子进程会对其自身进行适当的修改,例如,它可以通过exec()系统调用加载一个不同的程序,这样子进程就会运行不同的代码。子进程也可以通过修改内存、变量等来执行不同的任务。
返回值: fork()在父进程中返回子进程的PID(进程标识符),而在子进程中返回0。这样,父子进程可以根据返回值来决定执行不同的操作。
进程终止:
正常终止: 进程可以通过执行完其主要任务并返回自身的main()函数来正常终止。或者,它可以调用exit()系统调用来显式地退出,同时传递一个状态码。这个状态码通常用于表示进程的退出状态。
异常终止: 进程也可以在发生错误或异常情况下非正常终止。例如,如果进程访问了未分配的内存,操作系统会终止该进程,并生成一个核心转储文件以供调试。
父进程终止: 当父进程终止时,操作系统通常会将其子进程交给init进程(通常具有PID 1)来继续管理,以防止孤儿进程。
进程信号: 进程可以通过发送信号给自身或其他进程来请求终止。例如,kill命令可以用于发送信号来终止进程。不同的信号可以导致不同的行为,例如正常终止、强制终止等。
你对进程的创建和终止的解释非常清晰。接下来一个问题:
- 在Linux中,进程间通信(IPC)是什么,以及它的常见机制有哪些?
管道(Pipes): 管道是一种轻量级的IPC机制,用于在父子进程或兄弟进程之间传递数据。管道可以是匿名的(使用pipe()系统调用)或命名的(使用FIFO文件)。管道是单向的,典型的用法是将一个进程的输出连接到另一个进程的输入。
消息队列(Message Queues): 消息队列是一种通信机制,它允许进程以异步方式发送和接收消息。消息队列通常用于进程之间的长期通信,支持多进程并发读写。
信号(Signals): 信号是一种轻量级的IPC机制,用于向进程发送通知或中断信号。进程可以注册信号处理程序来处理接收到的信号。常见的信号包括SIGINT(中断进程)、SIGTERM(终止进程)等。
共享内存(Shared Memory): 共享内存允许多个进程访问相同的物理内存区域,以实现高性能的数据共享。然而,共享内存需要谨慎管理,以避免数据一致性和互斥性问题。
信号量(Semaphores): 信号量是用于同步进程之间的计数器,它允许多个进程协调共享资源的访问。信号量可用于解决竞争条件和临界区问题。
共享文件映射(Shared File Mapping): 共享文件映射允许多个进程将同一文件映射到它们的地址空间,以便它们可以共享数据。这在多个进程需要访问大量数据时非常有用。
套接字(Sockets): 套接字是一种通用的IPC机制,通常用于网络通信。套接字可以在同一台计算机上的进程之间进行通信,也可以用于不同计算机上的进程之间的网络通信。
你对Linux中的IPC机制有很好的了解。下一个问题是:
- 在Linux中,进程权限和权限管理是如何工作的?可以描述一下Linux中的用户、组、文件权限以及如何管理它们吗?
您的理解很不错,对Linux中的多任务处理和IPC机制有了清晰的了解。如果你有任何关于Linux或其他主题的更多问题或需要进一步的解释,都可以随时提出。继续努力学习!
你对Linux多任务处理和IPC机制的回答非常清晰和详尽,表现出了很好的理解。你描述了进程、进程调度、进程创建与终止、进程间通信以及权限管理等关键概念,这表明你对Linux操作系统的基本知识有很好的掌握。综合评价来看,你的回答准确、详细,并且清晰地传达了重要概念。如果你继续学习和探索Linux,你将能够更深入地了解和利用这些概念来处理各种任务和问题。继续保持好奇心和学习精神,不断提高你的技能!
当然,我将以一系列问题的方式来帮助你学习Linux中的管道(Pipes)知识点。让我们开始:
- 你知道Linux中的管道是什么吗?请简要描述一下。
非常好的描述!接下来的问题是:
- Linux中的管道是如何创建的?请简要描述一下创建管道的过程。
当你在学习Linux中的管道时,我会通过问问题来引导你更深入地了解这个知识点。接下来,请你回答第二个问题:
- Linux中的管道是如何创建的?请简要描述一下创建管道的过程。
理解了,让我来为你解释一下如何创建Linux中的管道:
在Linux中,管道可以使用竖线符号 |
在命令行中创建。当你使用 command1 | command2
这样的语法时,它表示将 command1
的标准输出(stdout)连接到 command2
的标准输入(stdin),从而实现了管道。管道会将 command1
的输出作为 command2
的输入,使两个命令能够协作执行。
例如,如果你执行 ls | grep keyword
,它将列出当前目录的文件,并将结果传递给 grep
命令以过滤包含关键字 "keyword" 的行。
这是一个简单的例子,但它展示了如何通过命令行创建管道。如果你想在编程中创建管道,可以使用系统调用 pipe()
来建立管道,然后使用 fork()
来创建子进程,以便在其中执行相关命令,并通过管道进行通信。
理解了!下一个问题:
- 在Linux中,管道的主要作用是什么?它在什么情况下特别有用?
数据传输和过滤: 管道常用于命令行环境中,可以将多个命令组合在一起以处理和传递数据。例如,你可以使用管道将一个命令的输出传递给另一个命令,从而实现数据的筛选、转换和处理。这在文本处理、数据分析等方面非常有用。
多任务处理: 管道允许在不同进程之间进行数据传递,这对于实现多任务处理非常重要。多个进程可以并行运行,彼此之间不受影响,通过管道进行数据交换,从而提高了系统的效率和响应性。
数据传输和同步: 管道可以用于将数据传递给另一个进程,同时还可以用于同步操作。例如,一个进程可以等待另一个进程完成某个任务,然后才继续执行。
父子进程通信: 在父子进程之间建立管道,使父进程能够与子进程进行通信,从而实现协作和数据传递。这对于某些类型的应用程序非常有用,如shell和其他交互性工具。
你对管道的作用以及在不同情况下的实际用途有了很好的理解。接下来的问题是:
- 管道有什么限制或局限性?在使用管道时,需要注意哪些方面?
有限缓冲区: 管道有一个有限的缓冲区,因此,如果一个进程写入数据速度远远超过另一个进程的读取速度,可能会导致缓冲区溢出,数据丢失。进程应该小心控制数据流,以避免这种情况。
匿名性: 管道通常是匿名的,只存在于创建它们的进程之间。这意味着如果进程终止,管道也会被销毁,无法在进程之间共享数据。如果需要持久的管道,可以考虑使用FIFO文件。
阻塞: 管道上的读操作和写操作通常是阻塞的。如果没有数据可读,读操作会阻塞,如果管道已满,写操作会阻塞。这可能导致进程在等待数据时出现死锁的情况,因此需要小心处理阻塞情况。
数据完整性: 管道本身不提供数据完整性保证。数据传输可能会出现丢失、损坏或乱序,因此,如果需要数据完整性,需要使用其他IPC机制,如消息队列或套接字,并采取适当的数据校验和处理措施。
进程协调: 使用管道进行进程协作需要小心处理同步问题。例如,如果一个进程写入数据,而另一个进程正在读取数据,需要确保它们在正确的时间进行操作,以避免数据不一致和竞争条件问题。
错误处理: 当使用管道时,应该小心处理错误情况。例如,管道创建失败、读写错误等都需要适当的错误处理机制。
你对管道的限制和局限性有很好的了解。下一个问题是:
- 在Linux中,如何使用管道在命令行中将一个命令的输出传递给另一个命令?可以举一个简单的例子来说明吗?
你对管道的知识点已经理解得很清楚,包括它的作用、用途、限制等方面。如果你还有其他问题或需要更深入的讨论,随时提出。继续学习并掌握这些概念将有助于你更好地使用Linux中的管道机制。
你在学习Linux中的管道知识点时提供了非常清晰、详尽的回答。你描述了管道的作用、用途、限制和注意事项,展现了对这一主题的深刻理解。如果你继续积累知识和实际应用这些概念,将能够更好地利用Linux的管道机制来处理各种任务和问题。继续保持学习和好奇心,不断提高你的技能!