一篇文章给你讲清楚-Linux进程

Linux进程

初识基本概念

1.什么是进程

  • 什么是程序、什么是进程,有什么区别
    • 程序是一系列指令的集合,这些指令告诉计算机如何执行特定的任务。程序通常是以文件的形式存储在硬盘上的,它们是由程序员编写的代码,经过编译后转换成计算机能够理解的机器语言。程序是静态的,意味着它们在磁盘上是不变的,只有当它们被执行时,才会变成动态的进程。
    • 进程是程序的一次执行实例。当程序被启动时,操作系统会创建一个进程,并为其分配必要的资源,如内存和CPU时间。进程是动态的,它们在执行期间可能会改变状态,比如从运行状态变为等待状态。每个进程都有自己的地址空间,这意味着它们可以独立于其他进程运行。
  • 2 如何查看系统中有哪些进程
    • ps -aux
    • top 类似win的任务管理器
  • 3 什么是进程标识符(PID)
    • 是用来唯一标识系统中的每个进程的非负整数,类似于每个人的身份证号。特殊的PID值有特定含义:
    • PID=0:通常被称为交换进程(swapper)或调度进程,主要负责进程调度和系统资源管理。
    • PID=1:是系统的初始化进程(init),负责启动和管理系统上的其他所有进程,完成系统初始化任务。
  • 4 什么叫父进程,什么叫子进程
    • 谁创建 谁是父
    • 被创建出来的是子

2.并行&并发

  • 并发(Concurrency)指的是多个任务在同一时间段内被执行,但不一定同时。在单核CPU上,通过时间分片的方式,可以实现任务的并发执行,即操作系统轮流给每个任务分配CPU时间,从宏观上看似乎是同时进行的。
  • 并行(Parallelism)指的是多个任务确实是在同一时刻同时进行的。这通常需要多个处理单元(例如,多核CPU或GPU中的成百上千个核心)同时执行不同的任务。

在这里插入图片描述

3.进程创建

3.1子进程的创建
  • 一个现有进程可以调用fork函数创建一个新进程,这个新进程被称为子进程(child process)。
3.2fork函数的返回值
  • 子进程中:返回0
  • 父进程中:返回子进程的ID
  • 出错时:返回-1
3.3父子进程的执行顺序
  • fork创建子进程后,父子进程的执行顺序没有固定规律,完全取决于操作系统的调度算法。
3.4父子进程的资源复制
  • 子进程是父进程的副本,它获得父进程数据空间、堆和栈的副本。
  • 需要注意的是,这些是子进程所拥有的副本,父子进程并不共享这些存储空间部分。
3.5正文段的共享
  • 当一个进程通过fork()创建子进程时,父子进程共享同一份程序代码(即正文段),以提高内存效率。
3.6为什么需要创建进程

为了实现并发执行和资源的独立分配,fork()函数用于创建一个新的进程,它是父进程的副本。

  • 使用fork()创建子进程的一些主要原因:

    • 并发性:通过创建子进程,可以同时运行多个程序或任务,提高系统的利用率和效率。

    • 隔离性:每个进程都有自己的地址空间,这意味着它们可以独立地运行而不会相互干扰。

    • 灵活性:子进程可以修改自己的数据段、堆和栈而不影响父进程或其他子进程,因为这些部分通常在创建时就进行了复制。

    • 资源管理:即使父进程结束,其子进程仍然可以继续运行,这允许资源的独立管理和持久性。

    • 错误隔离:如果一个进程崩溃,它通常只会影响自己,而不是整个系统或其它进程。

3.7 完全拷贝和写时拷贝

完全拷贝(父子进程完全独立)还是写时拷贝(父子进程共享内存)

vfork

  • vfork直接使用父进程存储空间、不拷贝
  • vfork保证子进程先运行,当子进程调用exit后,父进程才执行(会堵塞)

4.进程退出

4.1正常退出(5个)
  • Main函数调用return
  • 进程调用exit(),标准c库
  • 进程调用 _exit()或者_Exit(),属于系统调用
  • 补充:
    • 进程最后一个线程返回
    • 最后一个线程调用pthread_exit

exit()是对_exit()_Exit()的封装
并且exit() 会冲刷缓冲区,对运行产生的变量做处理

4.2异常退出
  • 调用abort
  • 当进程收到某些信号时,如ctrl+C
  • 最后一个线程对取消(cancellation)请求做出响应
4.3 进程状态
  • “S”状态代表可中断的睡眠状态(TASK_INTERRUPTIBLE)
  • “R”状态代表进程正在运行或者在运行队列中等待CPU资源。
  • “D”状态表示进程处于不可中断的睡眠状态(TASK_UNINTERRUPTIBLE),通常发生在磁盘IO操作期间,此时即使收到信号也不会唤醒进程。
  • “T”状态表示进程已被停止,一般是收到了SIGSTOP、SIGSTP、SIGTIN或SIGTOU信号。
  • “Z”状态是僵尸状态,表明子进程已经退出,但其父进程尚未收集其资源,导致其进程描述符仍然存在。
  • “X”状态代表进程已经结束,其任务在任务列表中不再可见。

5.进程等待

  • 为什么要等待子进程退出
    • 创建子进程目的,就是找人干活,干完活需要验收(收集子进程状态信息)
    • 使用wait waitpid收集子进程退出状态
    • wait waitpid 后子进程不会变为僵尸进程
  • 父进程等待子进程退出并收集子进程的退出状态
    • 子进程退出状态不被收集,变成僵死进程(僵尸进程)

注意:

关于“等待”的含义:等待指的是父进程通过特定的系统调用(如wait或waitpid)主动进入阻塞状态,直到子进程退出。这个过程中,父进程暂停执行,直到收到子进程退出的通知。

在这里插入图片描述

wait函数详解

wait 函数在Unix-like操作系统中用于挂起调用进程,直到其中一个子进程结束。

wait 函数的原型如下:

#include <sys/types.h>
#include <sys/wait.h>
pidt wait(int status);

参数:

  • status:这是一个指向 int 类型的指针,用于存储子进程的退出状态。如果 status 不是 NULL,那么在子进程结束后,wait 函数会将子进程的退出状态存储在这个指针指向的内存位置。如果 status 是 NULL,则 wait 函数不会返回子进程的退出状态。

返回值

  • wait 函数的返回值是一个 pidt 类型的值,表示子进程的进程ID(PID)。返回值的含义如下:
  • 如果 wait 成功返回了一个子进程的PID,则表示有一个子进程已经结束。
  • 如果 wait 返回 -1,则表示出错,可以通过 errno 变量来获取错误信息。
waitpid 函数详解

原型

pidt waitpid(pidt pid, int status, int options);

参数
pid

  • 类型:pidt
  • 描述:指定要等待的子进程的进程ID(PID)。
  • 取值:
    • -1:等待任意一个子进程结束。
    • >1:等待具有指定PID的子进程结束。
    • <0:等待进程组ID为 abs(pid) 的任意子进程结束。

status

  • 类型:int
  • 描述:一个指向 int 类型的指针,用于存储子进程的退出状态。
  • 取值:
    • 非 NULL:waitpid 会将子进程的退出状态存储在这个指针指向的内存位置。
    • NULL:不返回子进程的退出状态。

options

  • 类型:int
  • 描述:一组标志,用于控制 waitpid 的行为。
  • 取值:
    • 0:默认行为,阻塞等待直到子进程结束。
    • WNOHANG:非阻塞模式,如果没有任何子进程结束,则立即返回 -1,errno 设置为 ECHILD。
    • WUNTRACED:如果设置了这个标志,waitpid 也会返回处于停止状态的子进程的信息,即使它们没有退出。
    • WCONTINUED(某些系统特有):如果设置了这个标志,waitpid 也会返回继续执行的子进程的信息。

返回值

  • 类型:pidt

  • 描述:返回值表示子进程的PID。

  • 取值:

    • >0:成功返回了一个子进程的PID,表示该子进程已经结束。
    • 0:如果设置了 WNOHANG 标志,并且没有子进程结束,则返回 0。
    • -1:出错,可以通过 errno 变量来获取错误信息。
      错误码
  • 类型:errno

  • 描述:当 waitpid 返回 -1 时,errno 变量会被设置为相应的错误码。

  • 取值:

    • ECHILD:调用进程没有子进程,或者设置了 WNOHANG 标志且没有子进程结束。
    • EINVAL:pid 参数无效(例如,尝试等待一个不存在的进程)。
      waitpid 函数提供了比 wait 函数更多的灵活性,允许父进程指定等待特定的子进程,或者以非阻塞的方式等待子进程结束。通过设置不同的选项,父进程可以根据需要选择合适的等待策略。

6.孤儿进程

父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程
Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程

在这里插入图片描述

7.exec族函数

exec 系列函数用于在当前进程中执行另一个程序,而不会创建新进程。

根据不同的参数格式和执行环境,可以选择使用 execlexeclpexecvexecvp

1. execl
  • 原型

    int execl(const char *path, const char *arg0, ..., NULL);
    
  • 特点

    • 需要明确指定可执行文件的路径(不搜索 PATH 环境变量)。
    • 参数是一个以 NULL 结尾的可变参数列表(类似 argv 数组的展开)。
  • 使用技巧

    • 明确路径:适用于你知道程序的绝对路径或相对路径的情况。
    • 固定参数数量:适合程序参数较少、且你不需要动态构造参数列表的场景。
    • 以 NULL 结束:必须确保最后一个参数是 NULL,否则会出现未定义行为。
  • 示例

    execl("/bin/ls", "ls", "-l", NULL);
    
2. execlp
  • 原型

    int execlp(const char *file, const char *arg0, ..., NULL);
    
  • 特点

    • 类似于 execl,但会自动搜索 PATH 环境变量来查找可执行文件。
    • 参数也是一个以 NULL 结尾的可变参数列表。
  • 使用技巧

    • 搜索 PATH:适用于不想提供程序的绝对路径,而希望系统在 PATH 环境变量中找到可执行文件的场景。
    • 无需明确路径:可以简化代码,只提供可执行文件的名称。
  • 示例

    execlp("ls", "ls", "-l", NULL);  // 系统自动从 PATH 中找到 ls
    
3. execv
  • 原型

    int execv(const char *path, char *const argv[]);
    
  • 特点

    • 需要明确指定可执行文件的路径(不搜索 PATH 环境变量)。
    • 参数是一个 argv 数组(类似 main 函数的 argv 参数)。
  • 使用技巧

    • 动态参数:适用于参数列表动态生成或变量控制参数的场景,因为你可以通过构造一个 argv 数组来传递参数。
    • 数组灵活性:适合参数数量不固定的场景,可以根据程序逻辑动态构建 argv 数组。
  • 示例

    char *args[] = { "ls", "-l", NULL };
    execv("/bin/ls", args);
    
4. execvp
  • 原型

    int execvp(const char *file, char *const argv[]);
    
  • 特点

    • 类似于 execv,但会自动搜索 PATH 环境变量来查找可执行文件。
    • 参数也是一个 argv 数组。
  • 使用技巧

    • 动态参数和 PATH 搜索:适合动态构建参数列表并且希望系统自动从 PATH 中查找可执行文件的场景。
    • 组合功能:既具备数组的灵活性,又简化了路径查找的代码。
  • 示例

    char *args[] = { "ls", "-l", NULL };
    execvp("ls", args);  // 自动从 PATH 中找到 ls
    
总结
  • execlexeclp 适合固定参数且命令简单的情况。
  • execvexecvp 提供了更多的灵活性,适合动态参数列表的场景。
  • 当需要搜索 PATH 环境变量时,选择带有 p 后缀的函数(execlpexecvp)。

8.system函数的使用技巧

system() 函数是标准库中的一个函数,属于 <stdlib.h> 头文件。它用于在 C 程序中调用操作系统的命令解释器执行某些系统命令。

函数原型
int system(const char *command);
参数说明
  • command: 一个指向以空字符结尾的字符串指针,该字符串包含要执行的命令。如果 commandNULL,则 system() 只检查命令解释器是否存在。
返回值
  • 成功:返回命令的退出状态(由系统定义)。
  • 失败:返回非零值。具体的值取决于系统。
使用示例
  1. 执行简单命令
#include <stdlib.h>
#include <stdio.h>

int main() {
    int result = system("ls -l"); // 执行系统命令 ls -l
    if (result == -1) {
        perror("执行失败");
    }
    return 0;
}

该程序将调用系统的 ls -l 命令,列出当前目录下的文件和详细信息。

  1. 检查命令解释器是否存在
#include <stdlib.h>
#include <stdio.h>

int main() {
    int result = system(NULL); // 检查命令解释器是否存在
    if (result == 0) {
        printf("命令解析器不可用\n");
    } else {
        printf("命令解析器可用\n");
    }
    return 0;
}

如果 system(NULL) 返回 0,表示命令处理器不可用;否则表示命令处理器可用。

注意事项
  • 安全性system() 函数是一个潜在的安全风险,尤其是在处理来自不可信来源的输入时,因为它会在命令解释器中执行字符串中的命令。
  • 效率:使用 system() 会创建一个新的进程,并调用命令解释器,这可能会比较慢。如果只是为了执行简单的任务(比如列出文件),可以考虑直接使用 fork()exec() 系列函数来更高效地完成工作。
总结
  • system() 是一个简单的方式来执行系统命令,适合快速调用一些外部命令。
  • 对于更复杂的进程管理,建议使用 fork()exec() 函数。

9.popen函数使用技巧

popen() 函数是标准库中的一个函数,它用于创建一个管道,并打开一个进程以便执行系统命令。通过这个管道,程序可以读取或写入该进程的标准输入或标准输出。

函数原型
FILE *popen(const char *command, const char *type);
参数说明
  • command: 一个指向以空字符结尾的字符串,表示要执行的系统命令。
  • type: 一个字符串,可以是 "r""w",表示管道的模式:
    • "r":表示程序将从命令的输出中读取数据。
    • "w":表示程序将向命令的输入中写入数据。
返回值
  • 成功:返回指向 FILE 的指针,它是一个标准 I/O 流,可以用于读写操作。
  • 失败:返回 NULL,并设置 errno 来指示错误。
使用示例
  1. 从命令读取数据
#include <stdio.h>
#include <stdlib.h>

int main() {
    char buffer[128];
    FILE *fp;

    // 执行系统命令并读取输出
    fp = popen("ls -l", "r");
    if (fp == NULL) {
        perror("popen failed");
        exit(1);
    }

    // 逐行读取命令输出
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    // 关闭管道
    pclose(fp);

    return 0;
}

在这个例子中,popen() 执行 ls -l 命令,并通过管道读取其输出。

  1. 向命令写入数据
#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp;

    // 向 `sort` 命令的输入中写入数据
    fp = popen("sort", "w");
    if (fp == NULL) {
        perror("popen failed");
        exit(1);
    }

    // 向 sort 命令写入数据
    fprintf(fp, "banana\n");
    fprintf(fp, "apple\n");
    fprintf(fp, "cherry\n");

    // 关闭管道
    pclose(fp);

    return 0;
}

在这个例子中,popen() 调用了 sort 命令,并通过管道向它的标准输入中写入数据。

pclose()

popen() 创建的管道必须使用 pclose() 关闭。

int pclose(FILE *stream);
  • 参数:要关闭的管道流。
  • 返回值:返回命令的退出状态。如果失败则返回 -1
注意事项
  1. 阻塞行为popen() 是阻塞的,调用时会等待命令完成执行。
  2. 安全性:和 system() 类似,popen() 也可能存在安全隐患,尤其是在使用来自不可信输入的数据构造命令时。
  3. 使用场景popen() 常用于需要与命令进行数据交互的场景,而不仅仅是执行命令。
三者的区别

在这里插入图片描述

10.函数

getpid和getppid函数

函数功能

getpid 获得当前进程的id号

getppid 获得当前进程父进程的id号

函数原型

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);

参数说明

返回值

  • 对应的进程id号
fork函数

函数功能

创建一个新进程

函数原型

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

参数说明

返回值

  • 在子进程中,fork()函数返回0。
  • 在父进程中,fork()返回新创建的子进程的进程ID(PID)。
  • 如果发生错误,fork()返回-1。
vfork函数

函数功能

用于创建新进程的

函数原型

#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void);

参数说明

返回值

  • 在子进程中,vfork()函数返回0。
  • 在父进程中,vfork()返回新创建的子进程的进程ID(PID)。
  • 如果发生错误,vfork()返回-1。

特点

它与fork()函数类似,但有一些重要的区别。vfork()函数在父进程和子进程之间共享相同的地址空间,这意味着子进程可以直接访问父进程的变量、函数和堆栈等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT光哥吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值