当前位置: 首页 > news >正文

Linux操作系统实战:进程创建的底层原理(转)

在当今的技术领域中,Linux 系统犹如一座巍峨的高山,屹立于服务器、开发环境等众多关键场景的核心位置。据统计,全球超 90% 的超级计算机运行着 Linux 操作系统,在服务器市场中,Linux 更是凭借其高稳定性、安全性以及开源特性,占据了相当可观的份额 ,众多大型网站、企业级应用的服务器都基于 Linux 搭建。对于开发者而言,Linux 也是不可或缺的开发环境,大量的开源软件和丰富的开发工具都能在 Linux 上完美运行。

在 Linux 系统中,进程是其核心概念,是系统进行资源分配和调度的基本单位。可以说,Linux 系统中的一切活动几乎都离不开进程的参与,从启动一个简单的应用程序,到执行复杂的系统命令,再到管理系统资源,进程就像幕后的 “隐形引擎”,驱动着整个 Linux 系统的稳定运行。

对于想要深入理解 Linux 系统的人来说,探索进程的工作原理与应用实例,就像是找到了一把打开 Linux 神秘大门的钥匙。只有掌握了这把钥匙,才能在 Linux 的世界里游刃有余,无论是进行系统优化、开发高效的应用程序,还是解决复杂的系统问题,都能做到胸有成竹。接下来,就让我们一同踏上这场充满挑战与惊喜的 Linux 进程探索之旅。

一、Linux进程概述

1.1进程的定义

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体,在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

在Linux的世界里,进程就像是一个个充满活力的 “小助手”,它们是程序的执行实例,承载着程序在系统中的运行使命。简单来说,当你在 Linux 系统中启动一个程序时,系统就会为这个程序创建一个进程,这个进程包含了程序运行所需的各种资源和环境信息,如内存空间、文件描述符、CPU 时间等。

例如,当你打开浏览器访问网页时,浏览器程序就会被加载到内存中,并创建一个对应的进程,这个进程负责处理网页的请求、解析 HTML 代码、渲染页面等一系列任务;当你使用文本编辑器编写文档时,文本编辑器程序也会创建一个进程,用于处理用户的输入、保存文件等操作。可以说,进程是程序在运行时的具体体现,是操作系统进行资源分配和调度的基本单位。

(1)进程有怎么样的特征?

  • 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。

  • 并发性:任何进程都可以同其他进程一起并发执行

  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;

  • 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推 进

  • 结构特征:进程由程序、数据和进程控制块三部分组成;

  • 多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

(2)Linux进程结构?

Linux进程结构:可由三部分组成:代码段、数据段、堆栈段。也就是程序、数据、进程控制块PCB(Process Control Block)组成。进程控制块是进程存在的惟一标识,系统通过PCB的存在而感知进程的存在。

图片

系统通过PCB对进程进行管理和调度。PCB包括创建进程、执行程序、退出进程以及改变进程的优先级等。而进程中的PCB用一个名为task_struct的结构体来表示,定义在include/linux/sched.h中,每当创建一新进程时,便在内存中申请一个空的task_struct结构,填入所需信息,同时,指向该结构的指针也被加入到task数组中,所有进程控制块都存储在task[]数组中。

(3)进程的三种基本状态?

图片

  • 就绪状态:进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

  • 运行状态:进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以 执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

  • 阻塞状态:由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生 前即使把处理机分配给该进程,也无法运行。

1.2进程与程序的区别

虽然进程和程序密切相关,但它们之间却有着本质的区别。程序可以看作是一个静态的 “剧本”,它存储在磁盘等存储介质上,是一组有序的指令和数据的集合,本身并不执行任何操作,只是等待被执行。而进程则是这个 “剧本” 的动态 “演出”,它是程序在计算机上的一次执行过程,具有生命周期,包括创建、运行、等待、挂起、终止等状态。

为了更直观地理解它们的区别,我们可以以 Word 程序为例。当你安装好 Word 软件后,它就以程序文件的形式存放在你的硬盘中,这个程序文件不会自己运行,只有当你双击 Word 图标,系统才会将 Word 程序加载到内存中,并创建一个进程来执行它。此时,这个正在运行的 Word 进程就拥有了自己独立的内存空间、文件描述符等资源,它可以处理你输入的文字、设置字体格式、保存文档等操作。而且,你可以同时打开多个 Word 文档,每个文档都会对应一个独立的 Word 进程,这些进程虽然都源自同一个 Word 程序,但它们的运行状态和所处理的数据是相互独立的。这就好比一场戏剧,剧本只有一个,但不同的演出团队可以根据这个剧本进行不同的演绎,每个演出都是一次独特的 “进程”。

二、Linux进程的工作原理

2.1进程的创建

在 Linux 系统中,进程的创建主要通过 fork 函数来实现。fork 函数就像是一个神奇的 “分身术”,当一个进程(父进程)调用 fork 函数时,系统会为其创建一个几乎完全相同的副本,这个副本就是子进程 。

进程的创建过程:

  1. 分配进程控制块

  2. 初始化机器寄存器

  3. 初始化页表

  4. 将程序代码从磁盘读进内存

  5. 将处理器状态设置为用户态

  6. 跳转到程序的起始地址(设置程序计数器)

这里一个最大的问题是,跳转指令是内核态指令,而在第5步时处理器状态已经被设置为用户态。硬件必须将第5步和第6步作为一个步骤一起完成。

进程创建在不同操作系统里方法也不一样:

  • Unix:fork创建一个与自己完全一样的新进程;exec将新进程的地址空间用另一个程序的内容覆盖,然后跳转到新程序的起始地址,从而完成新程序的启动。

  • Windows:使用一个系统调用CreateProcess就可以完成进程创建。把欲执行的程序名称作为参数传过来,创建新的页表,而不需要复制别的进程。

Unix的创建进程要灵活一点,因为我们既可以自我复制,也可以启动新的程序。而自我复制在很多情况下是很有用的。而在Windows下,复制自我就要复杂一些了。而且共享数据只能通过参数传递来实现。

从原理上来说,fork 函数会复制父进程的地址空间、文件描述符表、寄存器等资源,使得子进程在创建之初几乎和父进程一模一样。不过,它们也并非完全相同,每个进程都有独一无二的进程 ID(PID),父子进程的 PID 自然是不同的,并且它们在后续的执行过程中也可以相互独立地对各自的资源进行修改。

举个例子,假设我们有一个父进程负责监控系统的运行状态,它定期检查系统的 CPU 使用率、内存使用情况等。当需要进行更深入的分析时,父进程可以调用 fork 函数创建一个子进程。父进程继续执行监控任务,而子进程则可以专注于对系统日志的分析,例如统计过去一小时内系统出现的错误信息数量、类型等。在这个过程中,父进程和子进程各自拥有独立的执行流,它们可以并发地执行不同的任务,从而提高系统的整体效率。

在代码实现上,使用 fork 函数非常简单。下面是一个简单的 C 语言示例代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid;// 调用fork函数创建子进程pid = fork();if (pid < 0) {// 创建子进程失败perror("fork failed");return 1;} else if (pid == 0) {// 子进程执行的代码printf("I am the child process, my PID is %d\n", getpid());} else {// 父进程执行的代码printf("I am the parent process, my PID is %d, and my child's PID is %d\n", getpid(), pid);}return 0;
}

在这段代码中,通过fork()函数创建了一个子进程。fork()函数返回后,会有两个执行流,父进程和子进程会分别从fork()函数调用处继续执行后续代码。根据fork()函数的返回值来判断当前是父进程还是子进程,父进程返回子进程的 PID,子进程返回 0。如果返回值小于 0,则表示fork()函数调用失败。

2.2进程的状态

在 Linux 系统中,进程就像一个拥有多种 “生活状态” 的个体,主要包含运行(Running)、就绪(Ready)、阻塞(Blocked)、停止(Stopped)和僵尸(Zombie)等状态 。

处于运行状态的进程,就像是舞台上正在表演的演员,它正在 CPU 上执行指令,充分利用 CPU 资源进行各种运算和数据处理。

就绪状态的进程则如同在后台候场的演员,它们已经万事俱备,准备好运行,只等待 CPU 这个 “导演” 分配时间片,一旦获得 CPU 资源,就可以立即投入运行。

阻塞状态的进程,就像被某个事件 “绊住了脚”,暂时无法继续执行。例如,当一个进程发起磁盘 I/O 请求时,由于磁盘的读写速度相对较慢,在数据传输完成之前,进程就会进入阻塞状态,等待 I/O 操作完成。在这个过程中,进程会放弃 CPU 资源,让 CPU 去处理其他更紧急的任务。

停止状态的进程,就像是被按下了 “暂停键”,它的执行被暂时挂起。通常,进程进入停止状态是由于接收到了某些特定的信号,比如 SIGSTOP 信号,这个信号可以由用户通过命令或者其他进程发送,用于暂停进程的执行;另外,当进程正在被调试时,也会进入停止状态,方便调试人员对其进行调试。

僵尸状态的进程则是一种比较特殊的存在,当一个子进程已经结束运行,但是它的父进程还没有调用 wait 或 waitpid 函数来获取其退出状态时,子进程就会进入僵尸状态。此时,子进程虽然已经不再执行任何代码,但是它的进程描述符(PCB)仍然保留在系统中,占用着一定的系统资源,就像一个 “行尸走肉” 一般。如果系统中存在大量的僵尸进程,就会浪费系统资源,甚至可能导致系统性能下降。

2.3进程调度

在 Linux 系统中,进程调度器就像是一个公正的 “裁判”,负责管理和分配 CPU 资源,确保各个进程都能得到合理的执行机会。它采用了一种精心设计的工作方式,通过一系列的算法和策略来决定哪个进程可以获得 CPU 时间片以及获得多长时间的 CPU 使用权。

其中,完全公平调度器(CFS)是 Linux 内核中用于普通进程调度的一种重要算法。CFS 的设计理念非常巧妙,它致力于确保所有进程在调度周期内都能获得公平的执行时间。为了实现这一目标,CFS 为每个进程设置了一个虚拟时钟(vruntime) 。当一个进程执行时,随着时间的推移,其 vruntime 会不断增加;而没有得到执行的进程,vruntime 则保持不变。调度器在选择下一个执行的进程时,总是会挑选 vruntime 最小的那个进程,因为这个进程的执行时间相对较少,这样就保证了每个进程都能在公平的原则下竞争 CPU 资源。

程序使用CPU的模式有3种:

  • 程序大部分时间在CPU上执行,称为CPU导向或计算密集型程序。计算密集型程序通常是科学计算方面的程序。

  • 程序大部分时间在进行输入输出,称为I/O导向或输入输出密集型程序。一般来说,人机交互式程序均属于这类程序。

  • 介于前两种之间,称为平衡型程序。例如,网络浏览或下载、网络视频。

对于不同性质的程序,调度所要达到的目的也不同:

  • CPU导向的程序:周转时间turnaround比较重要

  • I/O导向的程序:响应时间非常重要

  • 平衡型程序:两者之间的平衡

例如,假设有多个进程同时竞争 CPU 资源,进程 A、B 和 C 都处于就绪状态。进程 A 的优先级较高,进程 B 和 C 的优先级相对较低。在 CFS 调度算法的作用下,调度器会根据每个进程的权重(与优先级相关)和已经执行的时间来计算它们的 vruntime。虽然进程 A 的优先级高,但如果它已经执行了较长时间,其 vruntime 也会相应增加;而进程 B 和 C 虽然优先级低,但由于之前执行时间较少,它们的 vruntime 相对较小。

在某个时刻,调度器检查各个进程的 vruntime,发现进程 C 的 vruntime 最小,于是就会将 CPU 时间片分配给进程 C,让它得以执行。通过这种方式,CFS 调度算法确保了每个进程都能在一定的时间内获得执行机会,避免了低优先级进程长时间得不到调度的情况,实现了进程调度的公平性。

2.4进程间通信

在 Linux 系统中,进程间通信(IPC)就像是搭建起了一座桥梁,使得不同的进程之间能够进行数据交换和信息共享,协同完成复杂的任务。常见的进程间通信方式包括管道(Pipe)、信号(Signal)、共享内存(Shared Memory)、消息队列(Message Queue)和套接字(Socket)等 ,它们各自有着独特的特点和适用场景。

管道是一种非常基础且常用的通信方式,它可以分为匿名管道和命名管道。匿名管道主要用于具有亲缘关系的进程之间,比如父子进程。它就像一根单向的 “数据传输管道”,数据只能从一端写入,从另一端读出,遵循先进先出(FIFO)的原则。例如,当我们在编写一个简单的命令行工具时,父进程可以创建一个匿名管道,然后通过 fork 函数创建子进程。父进程将一些数据写入管道,子进程则从管道中读取这些数据进行处理,这样就实现了父子进程之间的数据传递。命名管道则允许没有亲缘关系的进程之间进行通信,它在文件系统中以文件的形式存在,就像是一个 “有名有姓” 的管道,不同进程可以通过这个命名管道来交换数据。

信号是一种异步的通信方式,它主要用于通知进程发生了某个特定的事件。例如,当用户在终端中按下 Ctrl+C 组合键时,系统会向当前运行的进程发送 SIGINT 信号,进程接收到这个信号后,可以根据预先设定的处理逻辑来进行相应的操作,比如终止进程的运行。信号就像是一个 “紧急通知”,它可以让进程在不需要持续轮询的情况下,及时响应某些重要事件。

共享内存是一种高效的进程间通信方式,它允许多个进程直接访问同一块内存区域,就像是多个进程共享了一个 “公共的黑板”,可以在上面自由地读写数据。由于数据直接在内存中进行传递,不需要经过内核的频繁拷贝,所以共享内存的通信速度非常快,特别适合需要大量数据传输和共享的场景。但是,共享内存也带来了一些问题,比如多个进程同时访问共享内存时可能会导致数据冲突和不一致,因此通常需要结合其他同步机制,如信号量(Semaphore)来保证数据的安全性和一致性。

消息队列则为进程间提供了一种可靠的消息传递机制,它就像是一个 “邮件收发室”,进程可以将消息发送到消息队列中,其他进程可以从队列中读取消息。每个消息都有一个特定的类型标识,接收进程可以根据这个类型来有选择性地读取自己感兴趣的消息。消息队列的优点是可以实现进程间的异步通信,并且可以处理不同类型的消息,适用于需要进行复杂数据交互和任务协调的场景。

套接字是一种功能强大的通信方式,它不仅可以用于本地进程间通信,还可以实现不同主机之间的进程通信,广泛应用于网络编程领域。套接字就像是一个 “万能的通信接口”,它支持多种通信协议,如 TCP 和 UDP。通过套接字,我们可以创建客户端 - 服务器模型的应用程序,实现不同计算机之间的数据传输和交互,比如常见的 Web 服务器与浏览器之间的通信,就是通过套接字来实现的。

三、Linux进程应用实例

3.1使用 fork 创建多进程实现并发处理

在 Linux 系统中,fork 函数为我们提供了强大的并发处理能力,让多个任务能够同时执行,大大提高了系统的运行效率。下面通过一个具体的代码示例,来深入了解如何使用 fork 创建多进程实现并发处理。

假设我们有多个文件需要处理,每个文件包含一些数据,我们希望通过多进程并发处理这些文件,加快处理速度。代码示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>#define FILE_COUNT 3// 模拟文件处理函数
void process_file(const char* file_name) {int fd = open(file_name, O_RDONLY);if (fd < 0) {perror("open file failed");exit(1);}char buffer[1024];ssize_t bytes_read;while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {// 这里可以进行具体的数据处理,比如统计单词数量、查找特定字符串等// 这里简单打印读取到的数据write(STDOUT_FILENO, buffer, bytes_read);}close(fd);
}int main() {const char* file_names[FILE_COUNT] = {"file1.txt", "file2.txt", "file3.txt"};pid_t pids[FILE_COUNT];for (int i = 0; i < FILE_COUNT; i++) {pids[i] = fork();if (pids[i] < 0) {perror("fork failed");return 1;} else if (pids[i] == 0) {// 子进程process_file(file_names[i]);exit(0);}}// 父进程等待所有子进程完成for (int i = 0; i < FILE_COUNT; i++) {waitpid(pids[i], NULL, 0);}printf("All files processed.\n");return 0;
}

在这段代码中,首先定义了一个process_file函数,用于模拟对文件的处理操作。在main函数中,通过一个循环调用fork函数创建了多个子进程,每个子进程负责处理一个文件。父进程则通过waitpid函数等待所有子进程完成文件处理任务。

实现并发处理的原理在于,fork函数创建的子进程与父进程相互独立,它们拥有各自的执行流,可以同时执行不同的任务。在这个例子中,多个子进程同时对不同的文件进行处理,避免了逐个处理文件的串行方式,大大提高了处理效率。这种并发处理方式在实际应用中非常广泛,比如在大数据处理场景中,需要对大量的数据文件进行分析和处理,使用多进程并发处理可以显著缩短处理时间;在服务器端开发中,当有多个客户端请求需要处理时,也可以通过多进程并发的方式,快速响应每个客户端的请求,提升服务器的性能和用户体验。

3.2进程间通信实例

(1)管道通信:管道是 Linux 进程间通信的一种基础方式,它为具有亲缘关系的进程(如父子进程)之间提供了一种简单而有效的数据传输通道;下面通过一个具体的代码示例,来展示如何使用管道实现父子进程通信:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>#define BUFFER_SIZE 1024int main() {int pipe_fd[2];if (pipe(pipe_fd) == -1) {perror("pipe creation failed");return 1;}pid_t pid = fork();if (pid == -1) {perror("fork failed");return 1;} else if (pid == 0) {// 子进程close(pipe_fd[1]); // 子进程关闭写端char buffer[BUFFER_SIZE];ssize_t bytes_read = read(pipe_fd[0], buffer, sizeof(buffer));if (bytes_read == -1) {perror("read from pipe failed");exit(1);}buffer[bytes_read] = '\0';printf("Child received: %s\n", buffer);close(pipe_fd[0]); // 子进程关闭读端exit(0);} else {// 父进程close(pipe_fd[0]); // 父进程关闭读端const char* message = "Hello, child!";ssize_t bytes_written = write(pipe_fd[1], message, strlen(message));if (bytes_written == -1) {perror("write to pipe failed");return 1;}printf("Parent sent: %s\n", message);close(pipe_fd[1]); // 父进程关闭写端wait(NULL); // 等待子进程结束}return 0;
}

在这段代码中,首先通过pipe函数创建了一个管道,pipe_fd数组的两个元素分别表示管道的读端和写端。然后通过fork函数创建子进程。在子进程中,关闭管道的写端,只保留读端,从管道中读取数据并打印;在父进程中,关闭管道的读端,只保留写端,向管道中写入数据,然后等待子进程结束。

在管道通信中,父子进程关闭相应文件描述符的操作至关重要。父进程关闭读端,子进程关闭写端,这样可以确保数据的单向传输,避免混乱和错误。从通信原理上来说,管道本质上是内核中的一块缓冲区,当父进程向管道写端写入数据时,数据被存储在这个缓冲区中;子进程从管道读端读取数据时,就是从这个缓冲区中获取数据。如果不关闭不需要的文件描述符,可能会导致读写冲突,例如父进程和子进程同时向管道写端写入数据,或者同时从读端读取数据,这会使数据的传输和处理变得混乱,无法保证通信的正确性。

(2)信号通信:信号是 Linux 进程间异步通信的重要方式,它能够让一个进程及时通知另一个进程发生了某个特定的事件;下面通过一个代码示例,展示如何使用信号实现进程间通知:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>// 信号处理函数
void signal_handler(int signum) {printf("Received signal %d\n", signum);
}int main() {// 设置信号处理函数if (signal(SIGUSR1, signal_handler) == SIG_ERR) {perror("signal setup failed");return 1;}pid_t pid = fork();if (pid == -1) {perror("fork failed");return 1;} else if (pid == 0) {// 子进程sleep(1); // 子进程先休眠1秒,确保父进程先发送信号printf("Child sending signal to parent\n");if (kill(getppid(), SIGUSR1) == -1) {perror("kill failed");exit(1);}exit(0);} else {// 父进程printf("Parent waiting for signal\n");pause(); // 父进程暂停,等待信号wait(NULL); // 等待子进程结束}return 0;
}

在这段代码中,首先定义了一个信号处理函数signal_handler,用于处理接收到的信号。通过signal函数将SIGUSR1信号与signal_handler函数关联起来,设置好信号处理函数。然后通过fork函数创建子进程,子进程休眠 1 秒后,使用kill函数向父进程发送SIGUSR1信号;父进程在创建子进程后,调用pause函数暂停执行,等待信号的到来,当接收到SIGUSR1信号时,会触发signal_handler函数的执行。

信号处理函数的设置原理是,当进程接收到指定的信号时,内核会暂停当前进程的执行,转而执行该信号对应的处理函数。在这个例子中,当父进程接收到SIGUSR1信号时,就会暂停当前的执行流,调用signal_handler函数,在函数中打印接收到信号的信息。信号的发送和接收原理是,发送进程通过kill函数向目标进程发送特定的信号,内核负责将信号传递给目标进程,目标进程根据信号的类型和设置的处理方式来进行相应的处理。

信号通信在实际应用中非常广泛,比如在服务器程序中,当服务器需要停止或重启时,可以通过发送信号通知相关的进程进行相应的处理;在守护进程中,也可以使用信号来实现进程的动态配置更新,当配置文件发生变化时,通过发送信号通知守护进程重新加载配置文件。

3.3守护进程的创建与应用

守护进程,也被称为精灵进程,是 Linux 系统中一种特殊的进程,它如同一位默默守护系统的 “隐形卫士”,在后台长期运行,独立于控制终端,并且周期性地执行特定的任务,为系统的稳定运行和各种服务的正常提供提供了坚实的保障。常见的守护进程有负责系统日志记录的 rsyslogd,它会持续监听系统中发生的各种事件,并将相关信息准确无误地记录到日志文件中,方便系统管理员进行故障排查和系统监控;还有网络服务守护进程 httpd,它时刻待命,等待处理来自客户端的 HTTP 请求,为用户提供网页浏览等服务。

守护进程具有一些独特的特点。它与控制终端脱离,这意味着它不会受到终端关闭、用户登录或注销等操作的影响,可以持续稳定地运行。它在后台运行,不会占用终端的输入输出资源,不会干扰用户在终端上进行其他操作。而且守护进程通常会忽略一些常见的信号,如 SIGHUP 信号,以确保自身的稳定性和持续性。

下面是一个创建守护进程的代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <syslog.h>int main() {pid_t pid;// 忽略SIGHUP信号,防止进程在终端关闭时被终止signal(SIGHUP, SIG_IGN);// 创建子进程pid = fork();if (pid < 0) {perror("fork failed");exit(1);} else if (pid > 0) {// 父进程退出,让子进程成为孤儿进程,被init进程收养exit(0);}// 子进程继续执行,创建新会话,使子进程成为新会话的组长,脱离终端控制if (setsid() == -1) {perror("setsid failed");exit(1);}// 更改工作目录为根目录,防止占用可卸载的文件系统if (chdir("/") == -1) {perror("chdir failed");exit(1);}// 设置文件创建掩码,确保创建的文件具有合适的权限umask(0);// 关闭不需要的文件描述符,防止占用资源close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 打开系统日志openlog("mydaemon", LOG_PID, LOG_DAEMON);syslog(LOG_INFO, "Daemon started");// 守护进程的主要逻辑,这里以每5秒记录一次系统时间为例while (1) {time_t now;struct tm *tm_info;time(&now);tm_info = localtime(&now);char time_str[64];strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);syslog(LOG_INFO, "Current time: %s", time_str);sleep(5);}// 关闭系统日志closelog();return 0;
}

在这个代码示例中,首先通过signal函数忽略SIGHUP信号,然后使用fork函数创建子进程,父进程退出,子进程继续执行。子进程通过setsid函数创建新的会话,使自己成为新会话的组长,从而脱离终端的控制。接着更改工作目录为根目录,设置文件创建掩码为 0,关闭标准输入、输出和错误输出的文件描述符,避免占用这些资源。之后打开系统日志,并在一个无限循环中,每隔 5 秒记录一次当前系统时间到系统日志中。

以系统日志记录守护进程为例,它在系统启动时就被创建并运行,持续不断地收集系统中的各种事件信息,如用户登录、系统错误、服务状态变化等,并将这些信息按照一定的格式记录到日志文件中。系统管理员可以通过查看日志文件,了解系统的运行状况,及时发现和解决潜在的问题。例如,当系统出现故障时,管理员可以通过分析日志文件,确定故障发生的时间、原因以及相关的操作记录,从而快速定位和解决问题。守护进程的应用不仅提高了系统的可靠性和稳定性,还为系统的管理和维护提供了有力的支持 。

四、深入理解与优化

4.1进程资源管理

在 Linux 系统中,进程如同一个个活跃的 “资源消费者”,它们在运行过程中会占用 CPU、内存、文件描述符等多种系统资源。理解进程对这些资源的占用和管理方式,对于优化系统性能、确保系统稳定运行至关重要。

CPU 是计算机系统中最为关键的资源之一,进程在执行过程中需要占用 CPU 时间来完成各种运算和指令执行。进程对 CPU 的占用时间直接影响着系统的整体性能和响应速度。在多进程环境下,不同进程对 CPU 资源的竞争十分激烈。例如,当系统中同时运行多个计算密集型进程时,它们会竞相争夺 CPU 时间片,导致每个进程获得的 CPU 执行时间相对减少,从而可能使系统整体性能下降,出现响应迟缓的情况。

内存也是进程运行不可或缺的资源,进程需要内存来存储程序代码、数据以及运行时产生的各种中间结果。进程对内存的占用包括虚拟内存和物理内存两部分。虚拟内存是进程可见的内存空间,它为进程提供了一个独立的地址空间,使得进程可以在自己的地址空间内自由地进行内存分配和访问,而无需担心与其他进程的内存冲突。物理内存则是实际的硬件内存,当进程访问虚拟内存时,操作系统会通过内存管理机制将虚拟地址映射到物理内存上。如果进程占用的内存过多,可能会导致系统内存不足,此时操作系统会采取一些措施,如将部分内存数据交换到磁盘上的交换空间(Swap)中,以释放物理内存供其他进程使用。然而,频繁的内存交换会极大地降低系统性能,因为磁盘的读写速度远远低于内存,这会导致进程的运行速度大幅下降。

文件描述符是 Linux 系统中用于管理文件和 I/O 资源的重要机制,当进程打开一个文件、创建一个套接字或者进行其他 I/O 操作时,系统会为其分配一个文件描述符,进程通过这个文件描述符来对相应的资源进行读写、控制等操作。每个进程都有一个文件描述符表,用于记录该进程所打开的文件描述符及其相关信息。如果进程打开的文件描述符过多,可能会耗尽系统的文件描述符资源,导致其他进程无法正常进行 I/O 操作。

在 Linux 系统中,我们可以使用一些强大的命令来查看进程的资源占用情况。top命令是一个实时监控系统资源使用情况的工具,它可以动态地显示系统中各个进程的 CPU 占用率、内存占用率、进程状态等信息。在终端中输入top命令后,会出现一个动态更新的界面,其中%CPU列表示进程的 CPU 占用率,%MEM列表示进程的内存占用率。通过按下P键,可以按照 CPU 使用率对进程进行排序,方便我们快速找出占用 CPU 资源最多的进程;按下M键,则可以按照内存使用率排序。

ps命令也是一个常用的查看进程信息的工具,它可以提供更详细的进程状态报告。使用ps -aux命令可以列出系统中所有进程的详细信息,包括用户、PID、CPU 占用率、内存占用率、虚拟内存大小、驻留内存大小等。其中,USER列显示进程的所有者,PID列是进程的唯一标识符,%CPU和%MEM分别表示 CPU 和内存的占用率,VSZ表示进程使用的虚拟内存总量,RSS表示进程使用的未被换出的物理内存大小。通过ps -aux | grep 进程名的方式,可以筛选出特定进程的信息,例如ps -aux | grep firefox可以查看火狐浏览器进程的资源占用情况。

除了查看进程的资源占用情况,我们还可以使用ulimit命令来设置进程的资源限制。ulimit命令是一个在 Unix - like 系统(包括 Linux 和 macOS)中内置的 shell 命令,用于控制和显示 shell 以及由 shell 启动的进程可以使用的系统资源限制。通过ulimit -n命令可以设置进程能够同时打开的文件数量限制。例如,ulimit -n 2048可以将当前进程的最大打开文件数设置为 2048。

这在一些服务器程序中非常重要,因为服务器程序通常需要同时处理大量的客户端连接,每个连接都会占用一个文件描述符,如果文件描述符的数量限制过低,程序可能会因无法打开新连接而出现错误。通过ulimit -m命令可以限制进程在虚拟内存中使用的最大字节数,ulimit -t命令可以限制进程可以使用的 CPU 时间(以秒为单位) 。这些限制可以防止某个进程过度占用系统资源,从而影响其他进程的正常运行。需要注意的是,ulimit命令的设置分为软限制和硬限制,软限制是用户可以调整到低于硬限制的任意值,而硬限制通常只有管理员可以改变,并且不能超过系统允许的最大值。

4.2进程优化策略

在 Linux 系统中,优化进程性能和资源利用率是提升系统整体效能的关键所在。通过采用一系列科学合理的策略,可以显著提高进程的运行效率,降低资源消耗,使系统更加稳定、高效地运行。

优化算法是提高进程性能的核心策略之一,一个高效的算法能够显著减少进程对 CPU 时间的占用。以数据排序为例,不同的排序算法在时间复杂度上存在巨大差异。冒泡排序是一种简单直观的排序算法,但其时间复杂度为 O (n²),在处理大规模数据时,随着数据量 n 的增加,排序所需的时间会呈平方级增长,这会导致进程长时间占用 CPU 资源,严重影响系统性能。而快速排序算法的平均时间复杂度为 O (n log n),在处理相同规模的数据时,其所需的 CPU 时间远远少于冒泡排序。在实际应用中,如果某个进程涉及大量的数据排序操作,将冒泡排序算法替换为快速排序算法,能够大幅减少进程的 CPU 执行时间,提高系统的整体响应速度,让系统能够同时处理更多的任务,提升用户体验。

合理分配内存是确保进程高效运行的另一个重要方面,内存分配不合理往往会导致内存碎片的产生。内存碎片是指在内存分配和释放过程中,由于内存块的大小不一致和分配策略的问题,导致内存中出现许多不连续的小块空闲内存,这些小块内存无法被充分利用,从而降低了内存的利用率。例如,在一个频繁进行内存分配和释放的进程中,如果每次分配的内存块大小不同,随着时间的推移,内存中就会逐渐形成大量的内存碎片。

当进程需要分配较大的内存块时,虽然系统中总的空闲内存量可能足够,但由于内存碎片的存在,无法找到连续的足够大的内存块,导致内存分配失败,进程运行出现异常。为了避免内存碎片的产生,可以采用一些优化的内存分配策略,如使用内存池技术。内存池是预先分配好一定大小的内存块,当进程需要内存时,直接从内存池中获取,而不是每次都向操作系统申请内存。当进程使用完内存后,将内存块归还到内存池,而不是立即释放回操作系统。这样可以减少内存分配和释放的次数,避免内存碎片的产生,提高内存的利用率和进程的运行效率。

在 I/O 操作方面,优化也至关重要。I/O 操作通常包括磁盘 I/O 和网络 I/O,它们的速度相对较慢,容易成为进程性能的瓶颈。在进行磁盘 I/O 时,采用异步 I/O 可以显著提高效率。异步 I/O 允许进程在发起 I/O 请求后,不必等待 I/O 操作完成,而是继续执行其他任务,当 I/O 操作完成后,系统会通过回调机制通知进程。这种方式避免了进程在 I/O 操作期间的阻塞,提高了 CPU 的利用率。

例如,在一个文件读写频繁的进程中,使用异步 I/O 可以让进程在等待磁盘读写的过程中,继续处理其他数据,而不是白白浪费 CPU 时间。在网络 I/O 方面,合理设置缓冲区大小可以减少网络通信的次数,提高数据传输效率。如果缓冲区设置过小,会导致数据频繁地在网络中传输,增加网络开销;如果缓冲区设置过大,又可能会导致内存占用过多,并且在数据传输不及时的情况下,造成数据积压。因此,需要根据实际的网络环境和应用需求,合理调整缓冲区大小,以达到最佳的网络 I/O 性能。

在多进程环境下,合理的进程调度策略也对性能有着重要影响。根据进程的优先级和任务类型进行调度,可以确保重要的进程优先获得 CPU 资源,提高系统的整体响应能力。对于实时性要求较高的进程,如视频播放、音频处理等多媒体进程,赋予它们较高的优先级,使其能够在 CPU 资源竞争中优先获得执行机会,保证多媒体播放的流畅性和实时性。而对于一些后台任务,如数据备份、日志分析等,可以降低它们的优先级,在系统资源空闲时再进行处理,避免与前台重要进程争夺资源,影响用户体验。

http://www.xdnf.cn/news/450109.html

相关文章:

  • 朱老师, 3518e系列,第三季
  • 【Python】杂乱-[代码]Python 替换字符串中相关字符的方法
  • 容器安全-核心概述
  • OpenCV人脸识别LBPH算法原理、案例解析
  • Codeforces Round 1003 (Div. 4)
  • 分布式一致性协议Raft
  • 动物乐园-第16届蓝桥第5次STEMA测评Scratch真题第5题
  • 11-SGM41299-TEC驱动芯片--40℃至+125℃-3A
  • 1. Go 语言环境安装
  • 数据清洗的艺术:如何为AI模型准备高质量数据集?
  • 《Python星球日记》 第71天:命名实体识别(NER)与关系抽取
  • 拓展篇、github的账号创建
  • Oracle中的select1条、几条、指定范围的语句
  • 【证书与信任机制​】证书透明度(Certificate Transparency):如何防止恶意证书颁发?​​
  • 【1000以内具有12个以上因子的整数并输出它的因子】2021-12-27
  • 如何在Mac电脑上的VScode去配置C/C++环境
  • 生成式AI:人工智能的新纪元
  • 请求内存算法题
  • 综述:拓扑材料的热磁性质
  • WordPress 和 GPL – 您需要了解的一切
  • 【leetcode】349. 两个数组的交集
  • WindTerm终端工具功能与优缺点分析
  • mysql的一个缺点
  • libmemcached库api接口讲解一
  • 开发者的测试复盘:架构分层测试策略与工具链闭环设计实战
  • c++之 sort()排序
  • Unity 小提示与小技巧[特殊字符]
  • 基于C#实现中央定位服务器的 P2P 网络聊天系统
  • 大二java第一面小厂(挂)
  • C++【STL】(2)string