首页 > 系统相关 >04-Linux系统编程之进程

04-Linux系统编程之进程

时间:2025-01-06 22:02:02浏览次数:3  
标签:printf 04 编程 pid num edu Linux 进程 id

一、进程的概述

1.什么是进程

进程:即进行中的程序,可执行文件从开始运行到结束运行这段过程就叫进程。

2.程序和进程的区别

  • 程序:存储在磁盘上、占磁盘空间、静态的。如:我们编写的C语言代码就是程序,存储在我们电脑磁盘上;

  • 进程:运行在系统上、占内存空间,动态的,包括进程的创建、调度、消亡。如:我们的代码经过编译生成了可执行文件,然后将可执行文件运行,这个运行中的程序就是进程。

3.并发和并行的区别

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,并行是真正做到了同时进行;
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,利用了人眼的暂留现象(余晖效应),因为切换太快,人眼感觉不出来,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行(分时复用)。

4.PCB 进程控制块

4.1 PCB 的概述

PCB:进程控制块,程序运行时,内核为每个进程分配一个 PCB(进程控制块),维护进程相关的信息。Linux 内核的进程控制块是 task_struct 结构体,这个结构体里面存放着运行维护进程需要的所有资源。

  • task_struct 结构体:这个结构体里面的内容很多,但很多是涉及到系统内核的一些操作,我们不需要全部了解,后续学习可能用到和需要掌握的内容主要如下:
    1. 进程 id:系统会为每个进程分配唯一的 id,在 C 语言中用 pid_t 类型表示,其本质是一个非负整数, 进程有就绪、运行、挂起、停止等状态;
    2. 进程切换时需要保存和恢复的一些 CPU 寄存器,因为我们上面讲到了分时复用,进程间快速切换,当前这个进程暂停了以后,下次要接着当前运行,就得保存当前的工作状态;
    3. 描述虚拟地址空间的信息,描述控制终端的信息,当前工作目录(CurrentWorking Directory),umask 掩码;
    4. 文件描述符表,包含很多指向 file 结构体的指针;
    5. 和信号相关的信息,用户 id 和组 id,会话(Session)和进程组,进程可以使用的资源上限(Resource Limit)等。

4.2进程的状态

上面提到了进程包括就绪、运行、挂起、停止等状态,这里就详细介绍一下。

4.2.1进程状态三层模型
  • 三层模型包括:

    1. 等待态:进程还不具备被 CPU 调度的条件,进程正在等待 CPU 能调用的条件成立;
    2. 就绪态:进程被调度的条件成立,等待 CPU 调度;
    3. 执行态:进程的正在被 CPU 执行。
  • 三层模型示意图

在这里插入图片描述

这里还有一个就绪态和执行态之间的一个循环切换,这里就是我们前面提到的分时复用,不同进程来回切换,每个进程只允许执行很短的事件,一个时间片到以后就把 CPU 让出来给其它进程用,该进程就变为就绪态等待被再次调用,如此循环。

4.2.2进程状态五层模型

相比于三层模型,多了僵尸态和停止态,等待态也分为了两种情况,其示意图如下:

在这里插入图片描述

  • 五层模型介绍:
    1. 可中断等待态(TASK_INTERRUPTIBLE) :进程被 CPU 调度的条件还不成立,但不一定要条件成立才能被唤醒,也会因为接收到信号而提前被唤醒;
    2. 不可中断等待态(TASK_UNINTERRUPTIBLE):和可中断相比,这个必须等到条件满足才能被唤醒,不能通过信号提前唤醒;
    3. 就绪态(TASK_RUNNABLE): 表示己经准备就绪,正等待被调度;
    4. 执行态(TASK_RUNNING) : 进程正在被 CPU 执行 ;
    5. 僵尸态(TASK_ZOMBIE):表示该进程已经结束了,但是其父进程还没有调用 wait 或 waitpid 来释放该进程资源(PCB资源);
    6. 停止态(TASK_STOPPED):进程停止执行,当进程接收到 SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 等信号的时候会进入停止态。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到 SIGCONT 信号,会重新回到执行态。
4.2.3查看进程状态

通过 ps -aux命令查看进程状态。

  • 命令演示
edu@edu:~$ ps -aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.1  0.1 119968  6004 ?        Ss   15:43   0:01 /sbin/init splash
root          2  0.0  0.0      0     0 ?        S    15:43   0:00 [kthreadd]
root          3  0.0  0.0      0     0 ?        S    15:43   0:00 [ksoftirqd/0]
root          5  0.0  0.0      0     0 ?        S<   15:43   0:00 [kworker/0:0H]
root          7  0.0  0.0      0     0 ?        S    15:43   0:00 [rcu_sched]
root          8  0.0  0.0      0     0 ?        S    15:43   0:00 [rcu_bh]
root          9  0.0  0.0      0     0 ?        S    15:43   0:00 [migration/0]
root         10  0.0  0.0      0     0 ?        S    15:43   0:00 [watchdog/0]
root         11  0.0  0.0      0     0 ?        S    15:43   0:00 [watchdog/1]
root         12  0.0  0.0      0     0 ?        S    15:43   0:00 [migration/1]
......
  • 上面的 STAT 就是其状态信息列,参数意义如下:
D     不可中断 Uninterruptible(usually IO)
R     正在运行,或在队列中的进程
S     处于休眠状态(大写S)
T     停止或被追踪
Z     僵尸进程
W     进入内存交换(从内核 2.6 开始无效)
X     死掉的进程
<     高优先级
N     低优先级
s     包含子进程(小写s)
+     位于前台的进程组(即与终端设备有交互的进程)
  • ps 命令常用于查看进程相关的信息,其选项如下:
-a     显示终端上的所有进程,包括其他用户的进程
-u     显示进程的详细状态
-x     显示没有控制终端的进程
-w     显示加宽,以便显示更多的信息
-r     只显示正在运行的进程
pstree 树状显示进程关系  
啥也不加,显示的是当前进程

二、进程号

1.进程号概述

每个进程都由一个唯一的进程号来标识,其类型为 pid_t。进程号总是唯一的,但进程号可以重用,即当一个进程终止后,其进程号就可以再次被其它使用。

  • 常用进程号分为:
  1. PID:当前进程号;
  2. PPID:当前进程的父进程号;
  3. PGID:进程组 ID。

2.获取进程号

2.1获取当前进程号

  • 函数介绍
#include <sys/types.h> // 包含的头文件
#include <unistd.h>
pid_t getpid(void);
功能:获取本进程号(PID)
参数:
	无
返回值:
	本进程的进程号(PID)

2.2获取当前进程父进程号

  • 函数介绍
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
功能:获取调用此函数的进程的父进程号(PPID)
参数:
	无
返回值:
	调用此函数的进程的父进程号(PPID)

2.3获取进程组号

  • 函数介绍
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
功能:获取进程组号(PGID)
参数:
	pid:0或指定进程号
返回值:
	参数为 0 时返回当前进程组号,否则返回参数指定的进程的进程组号
  • 代码演示
void test01()
{
    printf("当前进程号:%d\n", getpid());
    printf("当前进程父进程号:%d\n", getppid());
    printf("当前进程组号:%d\n", getpgid(0));

    // 用于阻塞,防止进程退出
    getchar();
}
  • 运行结果
当前进程号:4618
当前进程父进程号:2791
当前进程组号:4618
  • 通过 ps 命令查看当前进程相关进程号
edu@edu:~$ ps -ajx | grep a.out
 PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  2791   4618   4618   2791 pts/21     4618 S+    1000   0:00 ./a.out

可以看到,几个进程号是对应的。

查看父进程号对应的哪个进程:

edu@edu:~$ ps -A | grep 2791
  2791 pts/21   00:00:00 bash

可以看到,父进程号对应的进程是 bash 解析器,因此,每个进程都不能独立启动,都必须通过一个父进程间接启动,父进程还有父进程,就这样一层层有秩序地管理进程(创建进程和回收进程资源),一直到最顶层的 1 号进程。

  • 可以通过 pstree 命令查看进程间的创建关系
systemd─┬─ManagementAgent───6*[{ManagementAgent}]
        ├─ModemManager─┬─{gdbus}
        │              └─{gmain}
        ├─NetworkManager─┬─dhclient
        │                ├─dnsmasq
        │                ├─{gdbus}
        │                └─{gmain}
        ├─VGAuthService
        ├─accounts-daemon─┬─{gdbus}
        │                 └─{gmain}
... ...

三、创建子进程

1.子进程引入

  • 为什么我们要创建子进程,看下面的例子:
void test02()
{
    while (1)
    {
        printf("---------------------------1\n");
        sleep(1);
    }
    while (1)
    {
        printf("---------------------------2\n");
        sleep(1);
    }
}
  • 运行结果
---------------------------1
---------------------------1
---------------------------1
---------------------------1
---------------------------1
---------------------------1
... ...
  • 说明:可以看到,当程序执行时,永远都只能执行第一个循环,第二个循环被第一个阻塞掉了,就无法执行到,而如果我们想要同时执行两个循环,就需要用到子进程。

2.fork 创建进程

2.1fork 语法

进程是系统进行资源分配的基本单位。

子进程:系统允许一个进程创建新进程,这个新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。

  • fork 函数介绍
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程
参数:
	无
返回值:
	成功:在子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
	失败:返回-1。
失败的两个主要原因是:
	1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
	2)系统内存不足,这时 errno 的值被设置为 ENOMEM。
  • 注意:父进程和子进程都会在 fork 之后运行,创建子进程后,会另外开辟一个空间,将父进程的资源拷贝一份给子进程。

2.2创建一个子进程

  • 代码演示
void test03()
{
    pid_t pid = fork();
    if (pid > 0) // 父进程执行的代码
    {
        printf("父进程ID:%d\n", getpid());
        getchar(); // 阻塞,防止父进程退出
    }
    else if (pid == 0) // 子进程执行的代码
    {
        printf("子进程ID:%d\n", getpid());
        getchar(); // 阻塞,防止子进程退出
    }
}
  • 运行结果
父进程ID:6826
子进程ID:6827
  • 命令查看进程号
edu@edu:~$ ps -ajx | grep a.out
  2791   6826   6826   2791 pts/21     6826 S+    1000   0:00 ./a.out
  6826   6827   6826   2791 pts/21     6826 S+    1000   0:00 ./a.out
  • 当我们输入字符解堵塞
// 明明是一个父进程一个子进程,但是只输入了一个字符就退出了
// 难道父子进程都退出了马,通过命令查看进程状态
edu@edu:~$ ps -ajx | grep a.out
     1   6827   6826   2791 pts/21     2791 S     1000   0:00 ./a.out
  • 说明:

    1. 可以看到还有一个 a.out 进程在运行,对应进程号,可以看到是之前的子进程,说明刚刚只是父进程退出了,然后子进程没了父进程,就没有父进程为其回收资源了,为了防止无法回收子进程资源,系统会通过1号进程来接手,这样的进程叫做孤儿进程;
    2. 我们上面创建父子进程,然后分别执行了各自的代码,这里有一个误区,就是会误以为,if (pid > 0) 条件成立里面的部分是父进程,else if (pid == 0)条件成立里面的是子进程。其实创建子进程的时候,会将父进程的整个资源,包括这里的所有代码都拷贝一份到子进程,所有这里所有的代码在父子进程中都存在,只是从逻辑角度将其划分为父进程执行的代码和子进程执行的代码。
  • 上面同时执行两个 while 循环的代码的实现

void test04()
{
    pid_t pid = fork();
    if (pid > 0)
    {
        while (1)
        {
            printf("---------------------------1\n");
            sleep(1);
        }
    }
    else if (pid == 0)
    {
        while (1)
        {
            printf("---------------------------2\n");
            sleep(1);
        }
    }
}
  • 运行结果
edu@edu:~/study/my_code$ ./a.out
---------------------------1
---------------------------2
---------------------------2
---------------------------1
---------------------------1
---------------------------2
---------------------------1
---------------------------2
... ... 

3.父进程和子进程的关系

3.1父子进程的关系

使用 fork 函数创建的子进程是父进程的一个复制品,父进程的空间地址的内容拷贝了一份给子进程空间。

地址空间中包括:进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。

子进程所独有的只有它的进程号,计时器等。因此,使用 fork函数的代价是很大的。

但是为了尽可能减少空间的消耗,并不是将父进程的资源完完全全拷贝给子进程,对于一些数据在写时是独立的,读时是共享。

3.2 写时独立读时共享

  • 代码演示:读时共享
void test05()
{
    int num = 10;
    pid_t pid = fork();
    if (pid > 0)
    {
        while (1)
        {
            printf("父进程:num = %d,num_id = %p\n", num, &num);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
        while (1)
        {
            printf("子进程:num = %d,num_id = %p\n", num, &num);
            sleep(1);
        }
    }
}
  • 运行结果
父进程:num = 10,num_id = 0x7ffcfcff2ee0
子进程:num = 10,num_id = 0x7ffcfcff2ee0
父进程:num = 10,num_id = 0x7ffcfcff2ee0
子进程:num = 10,num_id = 0x7ffcfcff2ee0
父进程:num = 10,num_id = 0x7ffcfcff2ee0
子进程:num = 10,num_id = 0x7ffcfcff2ee0
父进程:num = 10,num_id = 0x7ffcfcff2ee0
子进程:num = 10,num_id = 0x7ffcfcff2ee0
... ...
  • 说明:上面是通过父子进程分别读取 num 变量的数据,同时打印 num 变量数据的地址,发现打印的数据和地址都一样,验证了上面所说的读时共享。也就是创建子进程的时候,只是将变量名拷贝了过去,但是通过变量名访问数据的时候还是访问的同一个内存地址。
  • 代码演示:写时独立
void test06()
{
    int num = 10;
    pid_t pid = fork();
    if (pid > 0)
    {
        while (1)
        {
            printf("父进程:num = %d,num_id = %p\n", num, &num);
            sleep(1);
        }
    }
    else if (pid == 0)
    {
        while (1)
        {
            num++;
            printf("子进程:num = %d,num_id = %p\n", num, &num);
            sleep(1);
        }
    }
}
  • 运行结果
父进程:num = 10,num_id = 0x7ffc89a37c30
子进程:num = 11,num_id = 0x7ffc89a37c30
父进程:num = 10,num_id = 0x7ffc89a37c30
子进程:num = 12,num_id = 0x7ffc89a37c30
父进程:num = 10,num_id = 0x7ffc89a37c30
子进程:num = 13,num_id = 0x7ffc89a37c30
父进程:num = 10,num_id = 0x7ffc89a37c30
... ...
  • 说明:可以看到,父子进程打印的两个 num 的值不一样了,说明子进程修改 num 变量了以后,num 在父子进程中已经是独立的两份了。但是这里看到的 num 的地址还是一样的,那是因为进程里面的内存地址是虚拟地址,并不是实际的物理地址,虽然父进程与子进程中变量虚拟地址是一样的,但是映射到不同的物理地址就得到了不同的数值。

3.3 printf 换行与不换行

  • 代码演示
void test07()
{
    printf("hello world\n"); // 加换行
    printf("hello friend");  // 不加换行
    pid_t pid = fork();
    if (pid > 0)
    {
    }
    else if (pid == 0)
    {
    }
}
  • 运行结果
hello world
hello friendhello friendedu@edu:~/study/my_code$
  • 说明
    1. 现象:可以看到,加了换行符的字符串打印了一遍,但没加换行符的字符串打印了两遍;
    2. 加换行符打印一遍,是因为我们知道 printf 函数是一个库函数,库函数有缓冲区,要将数据显示在终端设备上,需要将输出到缓冲区的数据刷新到终端,换行就是其中的刷新方式之一。在创建子进程之前,字符串就已经刷新到终端了,又因为父子进程是从 fork 之后执行的,因此对有换行的这个打印不会有任何影响,直接打印一次就完事了;
    3. 但是不加换行符,没有行刷新、满刷新和强制刷新,就只剩下进程结束刷新了,又因为在进程结束前先创建了子进程,子进程会拷贝父进程资源,连同缓冲区一起拷贝了,因此父子进程结束,会分别将字符串刷新到终端设备,就出现了两个 hello friend;
    4. 如果在其下面添加一个 fflush(stdout) 强制刷新,就只会打印一次。

3.4库函数 write 输出

  • 代码演示
void test08()
{
    write(1, "hello world", 11); // 加换行
    printf("hello friend");      // 不加换行
    pid_t pid = fork();
    if (pid > 0)
    {
    }
    else if (pid == 0)
    {
    }
}
  • 运行结果
hello worldhello friendhello friendedu@edu:~/study/my_code$
  • 说明:可以看到,如果使用库函数输出,即使不加换行也只会输出一次,因为库函数是直接操作内核资源,可以直接将数据输出到终端设备,根本不需要什么缓冲区,因此也就不存在库函数的缓冲区拷贝和结束刷新。

3.5 exit 和 _exit

  • 代码演示1
void test09()
{
    printf("hello friend");
    pid_t pid = fork();
    if (pid > 0)
    {
        exit(-1);
    }
    else if (pid == 0)
    {
        _exit(-1);
    }
}
  • 运行结果
hello friendedu@edu:~/study/my_code$
  • 代码演示2
void test09()
{
    printf("hello friend");
    pid_t pid = fork();
    if (pid > 0)
    {
        _exit(-1);
    }
    else if (pid == 0)
    {
        _exit(-1);
    }
}
  • 运行结果
edu@edu:~/study/my_code$ // 啥也没有
  • 代码演示3
void test09()
{
    printf("hello friend");
    pid_t pid = fork();
    if (pid > 0)
    {
        exit(-1);
    }
    else if (pid == 0)
    {
        exit(-1);
    }
}
  • 运行结果
hello friendhello friendedu@edu:~/study/my_code$
  • 说明:
    1. 上面演示的三种情况,可以发现,通过 _exit(-1) 退出进程的时候,不打印,通过exit(-1)会打印;
    2. 因为_exit(-1)是系统调用,作用是退出进程,不会刷新缓冲区;
    3. exit(-1)是库函数,作用是退出进程,会刷新缓冲区。

4.父子进程运行顺序

  • 代码演示
void test10()
{
    pid_t pid = fork();
    if (pid > 0)
    {
        printf("父进程运行了\n");
    }
    else if (pid == 0)
    {
        printf("子进程运行了\n");
    }
}
  • 运行结果
edu@edu:~/study/my_code$ ./a.out
父进程运行了
子进程运行了
edu@edu:~/study/my_code$ ./a.out
父进程运行了
子进程运行了
  • 说明:
    1. 上面运行的结果可以看出,我们多次调用,都是父进程先执行,子进程后执行,那是因为这里只有父进程先执行了才能调用 fork 创建子进程,因此这里演示肯定是父进程先执行,不然哪来的子进程;
    2. 但是我们站在原理的角度出发,父子进程是分别独立的进程,它们之间谁先运行要看谁先抢占到 CPU 资源,因此谁先运行是不确定的。

标签:printf,04,编程,pid,num,edu,Linux,进程,id
From: https://blog.csdn.net/qq_63958145/article/details/144973298

相关文章

  • Linux线程操作
    Linux线程操作要点:#include<pthread.h>//包含pthread.h头文件,用于线程操作ret1=pthread_create(&thread1,NULL,thread_function,"HelloThread1");//创建线程void*thread_function(void*arg){}//线程执行函数编译链接`pthread`线程:gcc2pth.c-o2out-lpthread......
  • AI 编程:如何用好 Lovable
    目前我最好的AI编程伙伴是Lovable和Cursor。bolt.new和windsurf,也都很不错,选择前两个是因为上限足够高。Lovable的网址,没有bolt.new有名字,我推荐大家都试试,尤其是不懂代码的人。我刚刚看了以下,我用lovable一共创建了40个项目。https://lovable.dev/​今天的......
  • 【花雕学编程】Arduino CNC 之支持加速度控制的G代码解析器
    Arduino是一个开放源码的电子原型平台,它可以让你用简单的硬件和软件来创建各种互动的项目。Arduino的核心是一个微控制器板,它可以通过一系列的引脚来连接各种传感器、执行器、显示器等外部设备。Arduino的编程是基于C/C++语言的,你可以使用ArduinoIDE(集成开发环境)来编写、......
  • 【花雕学编程】Arduino CNC 之循环运动绘制正方形
    Arduino是一个开放源码的电子原型平台,它可以让你用简单的硬件和软件来创建各种互动的项目。Arduino的核心是一个微控制器板,它可以通过一系列的引脚来连接各种传感器、执行器、显示器等外部设备。Arduino的编程是基于C/C++语言的,你可以使用ArduinoIDE(集成开发环境)来编写、......
  • 2025.1.6-3 Linux虚拟机网络配置
    VMware有三种主要的网络配置模式,分别为桥接模式(用的最多)、NAT模式(用的少)和仅主机(基本不用)模式。每种模式都有其特点和适用场景,以下为你详细介绍:1.桥接模式(Bridged)(最重要)原理:在桥接模式下,虚拟机的虚拟网卡会与主机的物理网卡进行桥接,虚拟机就如同局域网中的一台独立物理......
  • [读书日志]从零开始学习Chisel 第五篇:Scala面向对象编程——类继承(敏捷硬件开发语言Ch
    3.3类继承3.3.1Scala中的类继承为了节省代码量和反映实际各种类之间的联系,通常采取两种策略,包含和继承。包含是说明一个类中包含另一个类的对象,但两者之间没有必然联系。继承是从一个宽泛的类派生出更具体的类的过程,被继承的类称为“超类”或“父类”,而派生出来的类称为......
  • [读书日志]从零开始学习Chisel 第四篇:Scala面向对象编程——操作符即方法(敏捷硬件开发
    3.2操作符即方法3.2.1操作符在Scala中的解释在其它语言中,定义了一些基本的类型,但这些类型并不是我们在面向对象中所说的类。比如说1,这是一个int类型常量,但不能说它是int类型的对象。针对这些数据类型,存在一些基本操作符,比如算数操作符“+”。Scala所追求的是极致的面向对......
  • [读书日志]从零开始学习Chisel 第三篇:Scala面向对象编程——类和对象(敏捷硬件开发语言
    3.Scala面向对象编程3.1类和对象3.1.1类类是用class开头的代码定义,定义完成后可以用new+类名的方式构造一个对象,对象的类型是这个类。类中定义的var和val类型变量称为字段,用def定义的函数称为方法。字段也称为实例变量,因为每个被构造出来的对象都有自己的字段,但所有的对象公......
  • Linux环境变量配置
    0前言环境变量的配置主要便于操作系统正确的搜索到想要的文件,以及一些其他的配置。Linux中的环境变量配置分别有三种:临时的,个人用户的,全局的。相比Windows中的环境变量配置,多了临时的选项。1三种配置方法1.1临时配置export用户在终端可以通过export命令完成环境变量的临时......
  • Linux内核的固定映射:提升性能的秘密武器
    在当今数字化时代,高效稳定的Linux内核是众多技术应用的基石。你是否好奇,如何让Linux内核在复杂任务中实现卓越性能?今天,我们要揭开其提升性能的秘密武器——固定映射。它就像一位默默发力的幕后英雄,通过独特的机制,优化内核内存访问,让系统运行如丝般顺滑。下面,让我们一同走......