Linux系统编程:(十)进程概念

1. 冯·诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯·诺依曼体系。

截止目前,我们所认识的计算机,都是由一个个的硬件组件组成。

输入设备:键盘,鼠标,话筒,摄像头,网卡,磁盘等。

输出设备:显示器,磁盘,网卡,打印机等。

存储器:内存(磁盘就是外存)

中央处理器(cpu):运算器+控制器

首先思考几个问题:

1).软件运行,必须先加载到内存中,那么在程序运行之前,是存放在哪里的呢?

答案是存放在磁盘中,因为程序的本质其实就是文件。

2).为什么要加载到内存中呢?

因为是cpu执行我们的代码,访问我们的数据,而cpu获取,写入只能从内存中来进行。

2. 理解数据流动

数据的流动过程:

数据--->输入设备--->内存--->cpu--->内存--->输出设备--->数据

所以数据流动的本质:数据是从一个设备"拷贝"到另外一个设备。

所以体系结构的效率由设备的"拷贝"效率决定。

cpu在数据层面,只和内存打交道,而外设只和内存打交道。

3. 操作系统(Operator System)

3.1 概念

任何计算机系统都包含一个基本的软件集合,成为操作系统(OS),操作系统是一款进行软硬件管理的软件。

操作系统包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库,shell程序等)

3.2 设计OS的目的

  • 对下,与硬件交互,管理所有的软硬件资源。也可以认为这不是OS的目的,而是一种手段。
  • 对上,为用户程序(应用程序)提供一个良好的执行环境。

补充:

  1. 软硬件体系结构是层状结构,整个计算机世界都是高内聚,低耦合的!

  2. 操作系统本身不允许未来的用户直接访问内存,直接读取进程,直接访问文件,直接读取驱动,访问操作系统,必须使用系统调用---------其实就是函数,只不过是系统提供的。

  3. printf函数的本质:是用户把数据写到了硬件上(显示器)。我们的程序,只要判断出它访问了硬件,那么它必须贯穿整个软硬件体系结构。

  4. 库可能在底层封装了系统调用。

4. 理解操作系统

4.1 操作系统的核心功能

在整个计算机软硬件构架中,操作系统的定位是:一款纯正的"搞管理"的软件!

4.2 如何理解"管理"

打个比方:

操作系统就相当于学校校长,负责决策

驱动程序就相当于学校辅导员,负责执行

底层硬件就相当于学校学生,负责被管理

所以要管理,管理者和被管理者可以不需要见面,只需要让管理者拿到被管理者的"数据"即可进行管理。

那么如何拿到数据呢?这就由管理者和被管理者的中间层获取了------驱动程序。

建模的过程就是先描述,再组织,理解了这个过程就能够对任何"管理"进行建模,所以也能够理解为什么C++中有类和STL(容器)了,因为类解决的是描述,而STL解决的是组织的问题。这也就是这个世界的特点!!!

4.3 系统调用和库函数概念

操作系统要向上提供对应的服务,但是操作系统不相信任何用户。举个很贴合的例子就是:银行,银行为了给用户提供服务设置了一个个服务窗口。同样的,操作系统为了给用户服务,提供了"系统调用"功能。因为操作系统基本都是C语言写的,所以系统调用功能调用的基本都是C语言函数,而这些函数通常包含了输入参数和返回值,所以操作系统的本质:用户和操作系统之间,进行某种数据交互。

  • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
  • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

5. 进程

5.1 基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等。
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体。
  • 当前理解:进程=内核数据结构PCB(task_struct)+自己的程序代码和数据。

5.1.1 描述进程

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
  • 课本上称之为PCB(process control block),Linux操作系统下的PCB是:**task_struct。**进程的所有属性,都可以直接或者间接通过task_struct找到。
  • 对进程的管理,变成了对链表的增删查改!
  • 我们历史上执行的所有指令、工具、自己的程序运行起来全部都是进程。

5.1.2 task_struct

内容分类

  • 标示符:描述本进程的唯一标示符,用来区别其他进程。
  • 状态:任务状态,退出代码,退出信号等。
  • 优先级:相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据:进程执行时处理器的寄存器中的数据休学例子,要加图CPU,寄存器
  • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息
  • 具体详细信息后续会介绍

组织进程

可以在内核源代码⾥找到它。所有运⾏在系统⾥的进程都以 task_struct 双链表的形式存在内核 ⾥。

5.2 基本操作

1. 通过系统调用获取进程标识符

可以通过man指令查看这两个命令的使用方法:

bash 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
 printf("pid: %d\n", getpid());
 printf("ppid: %d\n", getppid());
 return 0;
}

我们在命令行运行这个程序的输出结果:

可以观察到,每当重新执行程序时,子进程的标识符(PID)都会发生变化,而父进程的标识符(PPID)则保持不变。这是因为在终端环境中,命令行解释器(Shell,如 bash 或 zsh)通常作为父进程启动并管理用户执行的命令 。当你输入命令(例如 ./a.out)时,Shell 会调用 fork()系统调用创建一个子进程来执行该程序,而自身则作为其父进程继续保持运行。因此,尽管每次运行程序都会生成一个新的子进程,其父进程始终是同一个 Shell 进程。

2. 如果想要查看当前系统中正在跑的进程信息,可以使用如下指令:

bash 复制代码
ps ajx

如果像查看指定的进程(例如名称为myprocess的进程):

bash 复制代码
ps ajx | head -1;ps axj | grep myprocess

上面的指令也可以写成:

bash 复制代码
ps ajx | head -1 && ps axj | grep myprocess

指令ps ajx | head -1指的是查看所有信息中的第一条信息,指令 ps ajx | grep myprocess指的是查看所有信息中名称为myprocess的进程,使用;或者&&可以使这两条指令同时进行,输出结果如下:

可以观察到,输出结果中包含了 grep指令自身对应的进程信息。这是因为在执行 grep命令时,其进程名称中也包含了"myprocess"这一关键字,因此被一并列出。如果希望过滤掉 grip自身相关的进程信息,可以使用如下指令实现:

bash 复制代码
ps ajx | head -1 && ps axj | grep myprocess | grep -v grep

除了上面这种方式之外,进程的信息可以通过 /proc 系统文件夹查看,这个文件夹中的数据都是内存中的数据,和磁盘没有关系:

bash 复制代码
ls /proc

-如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

图片中展示的均为 proc文件系统下的目录。其中,每一个以数字命名的目录代表一个特定进程的 PID(进程标识符)。这些数字目录中存储的是对应进程的动态属性信息;一旦该进程退出,系统便会自动将其对应的数字目录移除。若想查看特定进程的数字目录,可使用以下指令:

bash 复制代码
ls /proc/[pid] -dl

3. 如果想要结束一个进程:

  • 使用快捷键:ctrl+c
  • 使用命令行指令:
bash 复制代码
kill -9 [对应的进程pid]

4. 在查看 proc目录下某进程对应的数字目录时,我们会发现其中包含该进程可执行程序的路径(位于 exe文件中)。如果删除磁盘上这个路径指向的可执行文件,该进程并不会立即终止,原因在于其执行代码在启动时已被加载到内存中。此时再次查看进程属性,会发现 exe字段所显示的可执行文件信息已被标记为警告状态,表明对应的磁盘文件已不存在。

此外,我们还可以在进程属性中观察到另一个关键信息:cwd,它表示 current working directory,即该进程的当前工作路径。若需在程序运行时更改进程的当前工作路径,可通过系统调用 chdir() 函数来实现。在代码中调用此函数,即可动态指定程序运行时所处的目录位置。

bash 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 int main()
  5 {
  6   while(1)
  7   {
  8     chdir("/home/whb");                                                                                                                                                                                      
  9     sleep(1);                                                                          
 10     printf("我是一个进程 !,我的pid为: %d\n",getpid());                                
 11   }                                                                                    
 12   return 0;                                                                            
 13 }                                                                                      
 14    

再重新运行该程序然后查看对应的cwb发现已经发生了变化:

5. 通过系统调用创建进程--fork初始

1. 初识 fork

  • 运行 man fork :建议通过查阅手册深入了解 fork的官方定义与使用说明。
  • fork 的两个返回值 :这是 fork最核心的特性之一。调用一次,却在父子进程中各返回一次。

    • 父进程 :返回新创建子进程的 PID(进程 ID,大于 0)。

    • 子进程 :返回 0

    • 失败 :返回 -1(通常表示资源不足或权限问题)。

  • 进程间的关系

    • 代码段 :父子进程共享同一份代码(只读)。

    • 数据段 :各自开辟独立空间,私有副本(采用写时拷贝机制,即只有在写入操作时才复制数据,以节省内存)。

2. 代码实现:基础版与分流版

基础示例(认识 fork)

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    int ret = fork();
    printf("hello proc : %d, ret: %d\n", getpid(), ret);
    sleep(1);
    return 0;
}

进阶示例(fork 后的分流处理)

  • 注意fork之后通常要用 if进行分流,区分父子进程的逻辑。
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h> // 用于 perror

int main()
{
    int ret = fork();
    
    if (ret < 0) {
        perror("fork"); // 错误处理
        return 1;
    } 
    else if (ret == 0) { 
        // Child process
        printf("I am child : %d, ret: %d\n", getpid(), ret);
    } 
    else { 
        // Father process
        printf("I am father : %d, ret: %d\n", getpid(), ret);
    }
    
    sleep(1);
    return 0;
}

3. 重点注意

  1. 为什么 fork 会有两个返回值?

    • 因为 fork创建了两个 进程(父和子)。在子进程被创建出来的那一刻,它也开始执行代码,所以父子进程都会执行 fork()之后的语句,从而各自得到一个返回值。
  2. 这两个返回值是如何分别给父子进程的?

    • fork内部机制保证了:在父进程 的上下文中,返回值是子进程的 PID ;在子进程 的上下文中,返回值是 0。内核通过这种方式让它们"相认"。
  3. 一个变量怎么能同时让 if 和 else if 成立?

    • 关键陷阱 :代码中只有一个 ret变量,为什么 ret == 0else能分别执行?

    • 解释 :看似矛盾,实则是因为 fork之后实际上跑在两个不同的内存空间 (虽然物理页可能共享,但逻辑上是独立的)。父进程的 ret存的是子进程 PID(非0),子进程的 ret存的是 0。它们虽然变量名相同,但实际上是两个独立的执行流,互不干扰。这个问题需要在讲解完进程地址空间(虚拟内存)后才能彻底解释清楚。

相关推荐
学困昇10 小时前
Linux 信号机制详解:从 Ctrl+C 到 SIGCHLD,一文理解进程信号
linux·c语言·开发语言·人工智能·面试
艾莉丝努力练剑10 小时前
【Linux:文件】库的制作与原理进阶
linux·运维·服务器·网络·数据库·c++·人工智能
Strugglingler10 小时前
Linux Device Drivers-第八章 内存分配
linux·kernel·读书笔记·内存分配
z2023050810 小时前
RDMA之RDMA 的发展原因和软件架构基础(10)
linux·服务器·网络·人工智能·ai
炘爚10 小时前
Linux——线程
linux
GZ_TOGOGO11 小时前
sudo 命令详解与安全使用指南
linux·运维·安全
sheeta199811 小时前
LeetCode 每日一题笔记 日期:2026.05.28 题目:3093. 最长公共后缀查询
linux·笔记·leetcode
dnfdsaa11 小时前
【如何在Ubuntu 22上安装Claude Code并配置跳过官方引导】
linux·运维·ubuntu
AOwhisky11 小时前
Ceph系列第一期:Ceph分布式存储核心概念与架构初识
linux·运维·笔记·分布式·ceph·学习·架构