【Linux】进程

  代码是写给人看的,偶尔给机器跑一下。

前言 

  这是我自己学习Linux系统编程的第四篇笔记。后期我会继续把Linux系统编程笔记开源至博客上。 

  上一期笔记是关于Git原理与使用知识:

【Linux】Git原理与使用-CSDN博客https://blog.csdn.net/hsy1603914691/article/details/147590100?sharetype=blogdetail&sharerId=147590100&sharerefer=PC&sharesource=hsy1603914691&spm=1011.2480.3001.8118

冯诺依曼体系结构

1. 在 不考虑缓存的情况下, CPU仅能直接对 内存执行读写操作,无法直接访问外部设备。
2. 对于数据的输入和输出,必须通过外部设备向内存写入数据或从内存读取数据来完成。
3. 简而言之,所有设备间的交互均需经由内存进行。

操作系统

操作系统的内涵 

1. 任何计算机系统都包含 一个基本的程序集合 ,称为 操作系统
2. 操作系统 是一款进行 软硬件管理 的软件。
3. 操作系统 包括: 内核 ( 进程、内存、文件、驱动管理 )和 其他程序 ( 函数库、shell程序等 )。

1. 操作系统的作用: 

  • 对下,与硬件交互,管理所有的软硬件资源。
  • 对上,为用户程序提供一个良好的执行环境。

2. 操作系统的软硬件体系结构均为层状结构

3. 访问操作系统,必须使用系统调用,本质上就是调用系统提供的函数

4. 指令、代码等如果访问了硬件,那么它必须贯穿整个软硬件体系

系统调用 

1. 在开发角度,操作系统对外会表现为一个整体,但是 会暴露自己的部分接口 ,供上层开发使用,这部分 由操作系统提供的接口 ,叫做 系统调用
2.  系统调用在使用上,功能比较基础,对用户的要求相对也比较高。所以,有些开发者 对部分系统调用进行适度封装 从而形成库 。有了库,就有利于更上层用户或者开发者进行二次开发。
3. _exit() 函数是 系统调用函数 exit() 函数是对应的 库函数 exit() 函数在 刷新缓冲区 的的基础上,调用了 _exit() 的系统调用。

进程概念

进程的概念

1. 进程==程序的一个执行示例正在执行的程序

2. 进程==内核数据结构对象(task_struct)+进程的代码和数据

PCB的概念

1. 进程信息 存储在 进程控制块 ( PCB )中, PCB 是一个 包含了该进程所有属性的数据结构

2. PCBLinux操作系统中被称为task_struct,是管理进程状态的核心数据结构

父子进程 

1. 识别进程的方法是查看其进程ID每个进程都有唯一的进程ID

2. 子进程ID--->pid父进程ID--->ppid

3. 调用getpid()可以获取当前进程的ID,调用getppid()可以获取当前进程的父进程的ID

#include 
#include 
#include 

int main()
{
    while(1)
    {
        sleep(1);
        printf("这是一个进程!进程的pid为:%d\n",getpid());
        printf("这是一个父进程!父进程的pid为:%d\n",getppid());                                                                                              
    }
    return 0;
}

进程操作 

查询进程

ps -l查询进程信息

ls /proc/xxx列出 /proc 文件系统中与进程ID为xxx的进程相关联的目录内容

中断进程 

kill -9 xxx中止进程ID为xxx的进程

ctrl+c中止当前进程。 

创建进程 

1. fork()Linux系统中用于创建子进程的系统调用。调用fork()后,系统会复制当前进程,生成一个几乎完全相同子进程

2. 如果创建成功,fork()会给父进程返回给子进程的进程ID会给子进程返回0;如果创建失败,fork()会给父进程返回-1

3. fork()之前父进程独立执行fork之后父子两个执行流分别执行fork()被调用后,父子进程谁先执行完全由调度器决定

4. 父子进程的代码进行共享当数据未发生修改时--->数据共享当数据发生了修改--->子进程单独复制一份数据进行修改并使用(写时拷贝)。

//创建一个子进程
#include 
#include 
#include 

int main()
{
    int ret=fork();
    if(ret<0)
    {
        perror("fork");
        return 1;
    }      
    else if(ret==0)                                                                                                                                      
        printf("我是子进程,我的pid是%d\n",getpid());
    else                              
        printf("我是父进程,我的pid是%d,我的子进程的pid是%d\n",getpid(),ret);
    sleep(1);                                 
    return 0;                       
} 
//我是父进程,我的pid是29566,我的子进程的pid是29567
//我是子进程,我的pid是29567

进程中止 

退出状态码 

1. 进程终止的本质是释放其所占用的系统资源,包括进程相关的内核数据结构代码段数据段等。

2. 在Linux系统中,进程终止后会返回一个退出状态码,用于表示程序是否正常结束,以及执行结果是否符合预期。

3. 进程终止有三种常见情况:代码运行完毕结果正确代码运行完毕结果不正确代码异常中断提前终止

echo $?查看最近一个进程的退出状态码。 

//打印所有种类的进程退出状态码
#include 
#include 

int main()
{
    int i=0;
    for(i=0;i<200;i++)
        printf("%d--->%s\n",i,strerror(i));                                                                                                              
    return 0;
}

exit退出 

1. 在任何地方调用exit()函数都会立即终止当前进程并将退出状态码返回给其父进程,退出状态码用于告知父进程子进程是如何结束的。

2. 进程调用exit退出时,如果调用exit()函数,会自动刷新缓冲区;如果使调用_exit()函数,则不会刷新缓冲区。

#include 
#include 

int main()
{
    printf("没有刷清,_exit()退出");                                                                                                                      
    _exit(0);
}
include 
#include 

int main()
{
    printf("进行刷清,exit()退出");                                                                                                                       
    exit(0);
}

进程等待 

进程等待的必要性

1. 当子进程退出时,若父进程未对其进行处理,该子进程将进入"僵尸进程"状态,导致系统资源泄漏。而一旦成为僵尸进程,就无法再响应任何信号(kill -9),因为此时进程已终止,操作系统也无法再与其交互。

2. 父进程通常需要了解子进程的执行结果,例如是否正常退出、任务是否完成等。

3. 为此,父进程应通过进程等待机制回收子进程的资源,防止资源浪费,同时获取其退出状态,从而掌握子进程的具体运行情况。

wait等待 

1. wait()函数

  • 成功返回--->被等待进程的pid
  • 失败返回--->-1
  • 参数status--->NULL

2. waitpid()函数

  • 成功返回--->收集到的子进程的pid
  • 失败返回--->-1
  • 参数Pid--->-1等待任一个子进程与wait等效
  • 参数Pid--->>0等待其进程ID与参数Pid相等的子进程
  • 参数status--->NULL
  • 参数3--->0

status参数 

1. wait waitpid ,都有一个 status参数 ,该参数是一个 输出型参数 ,由操作系统填充。
2. 如果传递 NULL ,表示不关心子进程的退出状态信息。
3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
4. status参数 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图:

1. WIFEXITED(status):这是一个宏,用于判断子进程是否是正常终止的。如果是,则返回真。

2. WEXITSTATUS(status):这是一个宏,当WIFEXITED返回真时,使用此宏可获取子进程的退出状态码,用于分析子进程的执行结果。

阻塞

1. 当子进程已经终止时,调用wait()waitpid()会立即返回,不仅回收子进程所占用的系统资源,还能获取其退出状态,从而判断子进程的终止原因和执行结果。

2. 若在任意时刻调用 wait() waitpid() 时,若目标子进程仍在运行,则调用将导致父进程阻塞,直至子进程终止。
3. 若系统中不存在符合条件的子进程(如 子进程尚未创建 已全部退出 ),调用将立即失败,并返回相应的错误码。

阻塞定义:当一个进程或线程使用阻塞模式进行I/O操作(如读写文件、等待子进程等)时,它会暂停当前执行流直到操作完成或者满足某些条件(例如数据可读或子进程退出)。在此期间,该进程或线程不能执行其他任务

非阻塞轮调定义:在非阻塞模式下,进程或线程发起的操作不会导致自身被挂起,即使操作暂时无法完成。相反,它会立即返回并告知调用者当前操作的状态(成功、失败或正在进行中)。

进程程序替换 

1.  程序替换 是指通过 特定系统调用 将磁盘上的新程序(代码与数据)加载到当前进程的地址空间中 ,替换原有内容并开始执行新程序。
2. 在程序替换的过程中 并没有创建新的进程 ,只是把当前进程的代码和数据用新的代码和数据进行替换。
3. 一旦程序替换成功,就去执行新的代码, 原始代码的后续代码已经被替换了 不会被执行
4. 子进程进行程序替换 并不影响父进程 ,程序替换会让子进程的代码和数据 发生写时拷贝

exec系列函数

#include 
int execl(const char *path, const char *arg, ...);//第一个参数传入 路径+文件名
int execlp(const char *file, const char *arg, ...);//第一个参数传入 文件名
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);//第二个参数传入 字符指针数组
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

写时拷贝 

1. 父子进程在创建时会共享代码段和数据段只要双方都不对共享数据进行写操作,这些数据将在两者间保持共享。然而,一旦任意一方尝试修改共享数据,系统会通过写时拷贝机制为该进程创建一个独立的数据副本,从而避免影响其他进程的数据。

2.  写时拷贝技术使得父子进程在创建后能够共享资源,只有在需要写入数据时才会进行复制操作,从而实现了 进程之间的完全隔离。这不仅保证了 进程的独立性,还是一种高效的内存管理机制,通过 延迟物理内存的分配和复制,显著提高了整机内存的使用效率。
#include 
#include 
#include 
int a=10;
int main()
{
    int id=fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id==0)
        printf("%d %d %p\n",getpid(),a,&a);
    else
        printf("%d %d %p\n",getpid(),a,&a);
    sleep(1);                                                                                                                                            
    return 0;
}
//25976 10 0x601054
//25977 10 0x601054
#include 
#include 
#include 
int a=10;
int main()
{
    int id=fork();
    if(id<0)
    {
        perror("fork");
        return 1;
    }
    else if(id==0)
    {
        a=100;
        printf("%d %d %p\n",getpid(),a,&a);
    }
    else
    {
        sleep(3);                                                                                                                                        
        printf("%d %d %p\n",getpid(),a,&a);
    }
    sleep(1);
    return 0;
}
//26180 100 0x601054
//26179 10 0x601054

进程状态 

操作系统的进程状态 

1. 运行状态进程在调度队列中,要么是在运行中,要么在运行队列

2. 阻塞状态等待某种设备或者资源就绪

3. 阻塞挂起:内存不足时,把阻塞状态的进程的代码数据唤出到磁盘swap交换分区上

4. 运行挂起:内存不足时,把运行状态的进程的代码数据唤出到磁盘swap交换分区上

Linux系统的进程状态 

static const char *const task_state_array[] = {
    "R (running)", /*0 */
    "S (sleeping)", /*1 */
    "D (disk sleep)", /*2 */
    "T (stopped)", /*4 */
    "t (tracing stop)", /*8 */
    "X (dead)", /*16 */
    "Z (zombie)", /*32 */
};
1. R运行状态 ( running ): 进程要么是 在运行中 ,要么是 在运行队列中
2.  S睡眠状态 ( sleeping ):又称为 可中断休眠 ,意味着 进程在等待事件完成
3. D磁盘休眠状态 ( Disk sleep ):又称为 不可中断休眠 ,在这个状态的 进程通常会等待IO的结束
4. T停止状态 ( stopped ):可以通过 发送SIGSTOP信号 给进程来停止进程。这个被暂停的进程可以通过 发送SIGCONT信号 让进程继续运行。
5. X死亡状态 ( dead ):这个状态只是一个 返回瞬间状态 ,不会在任务列表里看到这个状态。
6. Z僵尸状态 ( zombie ):这个状态是一种 特殊状态 子进程已经结束执行代码和数据已经释放掉但是子进程PCB仍然存在 等待父进程收集退出信息

僵尸进程详解

1. 僵尸状态(zombie):子进程已经结束执行但其退出信息还未被父进程收集

2. 僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

3. 如果父进程创建了很多子进程但是不去回收,就会造成内存资源的浪费内存泄漏

//创建一个僵尸进程
#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    if (id < 0) 
    {
        perror("fork");
        return 1;
    } 
    else if (id > 0) 
    {     
        // 父进程
        printf("Parent[%d] is sleeping...\n", getpid());
        sleep(30); // 父进程睡眠30秒,此时子进程可能已经退出,变成僵尸进程

        // 可选:等待子进程结束以避免僵尸状态
        // wait(NULL);
    } 
    else 
    { 
        // 子进程
        printf("Child[%d] is running and will become Z...\n", getpid());
        sleep(5); // 子进程运行5秒后退出
        exit(EXIT_SUCCESS);
    }
    return 0;
}

孤儿进程详解

1. 孤儿进程父进程先退出子进程就称之为孤儿进程 。

2. 孤儿进程会被1号进程领养会变成后台进程

#include 
#include 
#include 

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        //child
        printf("I am child, pid : %d\n", getpid());
        sleep(10);
    }
    else
    {
        //parent
        printf("I am parent, pid: %d\n", getpid());
        sleep(3);
        exit(0);
    }
    return 0;
}

进程优先级

优先级的概念

1. 优先级进程得到CPU资源的先后顺序

2. 优先级--->谁先谁后的问题权限--->能不能的问题

PRI和NI 

1. PRI进程优先级默认80

2. NI进程优先级的修正数据最小值为-20最大值为19

3. 进程的真实的优先级 == PRI(默认)+NI,因此Linux操作系统进程优先级范围[60,99]

4. 进程的真实的优先级的数值越低--->优先被执行

修改优先级

1. 使用top命令可以方便地调整进程的优先级.

2. 首先在终端中输入top启动该工具--->然后按r键进入重新设置优先级模式--->接着输入你想要调整的进程的PID--->最后输入新的nice值来改变该进程的优先级。 

竞争、独立、并行、并发概念

1. 竞争 : 系统进程数目众多,而 CPU 资源只有少量,甚至只有1个, 所以 进程之间是具有竞争属性的 。为了高效完成任务,更合理竞争相关资源,便具有了优先级。
2. 独立 : 多进程运行,需要独享各种资源, 多进程运行期间互不干扰
3.  并行 : 多个进程在 多个CPU 下分别,同时进行运行,这称之为并行。
4. 并发 : 多个进程在 一个CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

进程切换

1. CPU内部存在大量的寄存器寄存器就是CPU内部的临时空间

2. 寄存器 != 寄存器内部的数据。 

3. 切换进程最核心步骤的就是保存和恢复当前进程的上下文数据,即寄存器里面的内容

4. 进程不会持续的在CPU上执行。当其时间片用尽时,操作系统会将该进程从CPU上移除,以便其他进程可以运行。

进程调度

运行队列

1. 每个CPU都有一个独立的运行队列(runqueue),内部维护着两个队列:一个用于存放当前可运行的进程(活动队列),另一个用于存放时间片已耗尽的进程(过期队列)。 

2. 过期队列上放置的进程,都是时间片已耗尽但尚未完全执行完毕(即还需要再次调度)的进程。

3. 运行队列中存在两个指针active指针永远指向活动队列expired指针永远指向过期队列

4. 活动队列上的进程会越来越少过期队列上的进程会越来越多。在合适的时候,只要交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程。

队列调度 

1. 活动队列三部分构成:

  • nr_active:总共有多少个运行状态的进程。
  • queue[140]:每个元素都是一个相同优先级的进程队列,按照FIFO规则进行排队调度。
  • bitmap[5]140个优先级就有140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空
2.  普通 优先级: 100-139 ,对应 PRI 60-99
3.  实时 优先级: 0-99 ,在 Linux 系统中 并不需要考虑

4. 过期队列活动队列的内部结构一模一样。

5. 在系统中查找最适合调度的进程所需的时间复杂度为O(1),即该过程所花费的时间是固定的,不会随着进程数量的增加而增长。我们把这种调度算法称为O(1)调度算法

环境变量

命令行参数的概念

1. 命令行参数:在运行程序时,通过命令行传递给程序的参数

2. 命令行参数让用户在不修改源代码的情况下,能够对程序的行为进行控制或配置。

3. main函数通过解析命令行参数,从而根据不同的参数值实现不同的功能。

#include 
#include 

int main(int argc, char *argv[]) 
{
    if (argc != 2) 
    {
        printf("Usage: %s [-a|-b|-c]\n", argv[0]);
        return 1;
    }
    const char* arg = argv[1];
    if (strcmp(arg, "-a") == 0) 
        printf("这是功能1\n");
    else if (strcmp(arg, "-b") == 0) 
        printf("这是功能2\n");
    else if (strcmp(arg, "-c") == 0) 
        printf("这是功能3\n");
    else 
    {
        printf("Usage: %s [-a|-b|-c]\n", argv[0]);
        return 1;  
    }
    return 0; 
}

环境变量的概念 

1. 环境变量:在操作系统中,用来指定操作系统运行环境的一些参数

2. 环境变量可以被子进程继承,环境变量通常是具有全局属性的

常见的环境变量 

1. PATH定义了系统命令的搜索路径。通过将常用命令文件的目录添加到PATH环境变量中,用户可以在任何位置直接调用这些命令,而无需输入完整路径。

2. HOME指定了用户的主工作目录,即用户登录Linux系统时默认进入的目录

3. SHELL:表示当前用户默认使用的Shell程序,其值通常是 /bin/bash 

4. 每个程序启动时都会获得一个环境表。该表由多个字符指针组成,每个指针指向一个以 '\0' 结尾的环境变量字符串

查看修改环境变量 

env查看所有的环境变量

echo $xxx查看变量名为xxx的环境变量

export设置一个新的环境变量。 

通过代码中获取环境变量

获取所有环境变量的值 

#include 
  
int main(int argc, char *argv[], char *env[])
{
    for(int i=0;env[i];i++)
        printf("%s\n",env[i]);
    return 0;                                                                                                                                          
}

获取部分环境变量的值  

#include 
#include                                                                                                                                       

int main()
{
    printf("%s\n",getenv("PATH"));
    return 0;
}

本地变量

1. 本地变量和环境变量相似,但是不能被子进程继承只能在bash内部使用

set显示本地定义的shell变量和环境变量

unset xxx清除shell变量xxx或环境变量xxx

虚拟地址空间

虚拟地址概念

1. 地址分为虚拟地址物理地址。在C/C++程序中,我们看到的所有地址都是虚拟地址物理地址对用户不可见,由操作系统统一管理。 

mm_struct结构体 

struct mm_struct
{
    /*...*/

    struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
    struct rb_root mm_rb; /* red_black树 */
    unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/

    /*...*/

    // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;

    /*...*/
}

1. 虚拟地址空间Linux操作系统中通过mm_struct结构来表示和管理。

2. 每个进程的task_struct中包含一个指向其mm_struct的指针,后者存储了该进程的虚拟地址空间信息

4. mm_struct结构体中,记录了每段虚拟地址空间的起始和结束地址。通过页表机制,这些虚拟地址被映射到实际的物理地址上,从而实现内存的有效管理和隔离。

虚拟地址管理

1. 在Linux内核中,vm_area_struct结构用于表示独立的虚拟内存区域(VMA)。

2. 每个VMA代表一段具有相同属性的虚拟地址空间,如代码段、数据段、堆栈或内存映射文件等。

3. 鉴于不同类型的虚拟内存区域拥有各自的功能和管理机制,一个进程会使用多个vm_area_struct结构来分别描述这些不同类型的虚拟内存区域

虚拟地址意义

1. 当直接使用物理内存来运行多个程序时,要求所有并发运行的程序占用的内存总量不得超过计算机实际的物理内存大小。这种限制严重制约了系统支持多任务处理的能力,并可能导致资源竞争问题。

2. 直接访问物理内存缺乏有效的读写权限管理,这意味着任何程序都能够不受限地读取或修改内存中的数据,这种情况容易导致安全漏洞,增加遭受攻击的风险。

自定义shell

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define FORMAT "[%s@%s %s]#"

int g_argc=0;
char* g_argv[128];

const char* GetUserName()
{
    const char* name=getenv("USER");
    return name==NULL?"None":name;
}
const char* GetHostName()
{
    const char* hostname=getenv("HOSTNAME");
    return hostname==NULL?"None":hostname; 
}
const char* GetPwd()
{
    const char* pwd=getenv("PWD");
    return pwd==NULL?"None":pwd;
}

void MakeCommandLine(char CmdPrompt[],int size)
{
    snprintf(CmdPrompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
}
void PrintCommandPrompt()
{
    char prompt[1024];
    MakeCommandLine(prompt,sizeof(prompt));
    printf("%s",prompt);
    fflush(stdout);
}
bool GetCommandline(char* out,int size)
{
    //ls -l ---> "ls -l\n" 字符串
    char* c=fgets(out,size,stdin);
    if(c==NULL)
        return false;
    out[strlen(out)-1]=0;
    if(strlen(out)==0)
        return false;
    return true;
}
bool CommandParse(char* commandline)
{
    //"ls -l" ---> "ls" "-l"
    #define SEP " "
    g_argc=0;
    g_argv[g_argc++]=strtok(commandline,SEP);
    while((bool)g_argv[g_argc++]=strtok(nullptr,SEP));
    g_argc--;
    return true;
}
void Execute()
{

    pid_t id=fork();
    if(id==0)
    {
        //5.子进程进行程序替换
        execvp(g_argv[0],g_argv);
        exit(1);
    }
    pid_t rid=waitpid(id,NULL,0);
}

int main()
{
    while(true)
    {
        //1.输出命令行提示符
        PrintCommandPrompt();
        //2.获取用户输入指令
        char commandline[1024];
        if(!GetCommandline(commandline,sizeof(commandline)))
            continue;
        //3.命令行分析
        CommandParse(commandline);
        //4.执行命令
        Execute();
    }
    return 0;
}

致谢

  感谢您花时间阅读这篇文章!如果您对本文有任何疑问、建议或是想要分享您的看法,请不要犹豫,在评论区留下您的宝贵意见。每一次互动都是我前进的动力,您的支持是我最大的鼓励。期待与您的交流,让我们共同成长,探索技术世界的无限可能!

你可能感兴趣的:(Linux,linux)