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。它们虽然变量名相同,但实际上是两个独立的执行流,互不干扰。这个问题需要在讲解完进程地址空间(虚拟内存)后才能彻底解释清楚。

相关推荐
A小辣椒3 小时前
TShark:基础知识
linux
AlfredZhao5 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao20 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix