4.Linux进程概念

冯诺依曼体系结构

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

image-20230128204111053

冯诺依曼体系结构计算机分为五大单元:

  1. 这里的存储器指的其实就是内存(但是如果断电,容易失去内存中的数据,即掉电易失)
  2. 磁盘也称作外存,它具有永久性的存储能力,磁盘是属于外设的一种
  • 外设:输出设备和输入设备都叫做外设
  • 输出设备:显示器
  • 输入设备:键盘,摄像头,话筒等等
  • 磁盘和网卡:即是输出设备,也是输入设备
  • 从磁盘中读取数剧就是输出,往磁盘中写入数据就是输入
  • 相对于运算器和CPU,这些设备被称之为外设

3.运算器 + 控制器 + 其他 = CPU

  • CPU是如何完成执行别人的指令,计算别人数据的目的? 如: b = 333 * 2
  • CPU其实是很笨的,只能够被动的接受别人的指令,别人的数据
  • 因此CPU必须要认识别人的指令(所以CPU中有自己的指令集)
  • 我们写代码,编译的本质就是将其转化为CPU能够认识的二进制可执行程序

CPU在读取和写入的时候,在数据层面和内存打交道,为什么?

​ 答:为了提高整机效率

​ CPU的运算速度(纳秒级别) > 内存(微秒级别) >> 外设(磁盘)(毫秒级别甚至秒级别 ), 根据木桶原理,整机的效率一定是由速度最慢的设备决定的,即外设决定的,如果外设和CUP直接交流,整机效率会非常慢,因此我们可以将外设中的数据提前加载到内存中(也就是缓存到内存中),这样就可以提高加载到CPU的效率,同样的原理,也可以将CPU的数据写入到内存中,再将其写入外设,就可以提高写入到外设的效率

但是此时又存在一个问题,谁来将数据加载到内存,谁来将内存的数据写入到磁盘等等一系列的管理?

​ 答:操作系统就是专门来帮助我们来完成这一切的

注:将数据从内存写入到外设,将外设的数据加载到内存就称作IO(即input,output)

​ 综上我们可以得出如下结论:

​ 1.CPU不和外设打交道,只和内存打交道

​ 2.所有的外设有数据载入,只能载入到内存,内存写出,也一定是写入到外设中,这也是由硬件的体系结构决定的。

操作系统(operating system)

1.什么是操作系统?

操作系统是一个进行软硬件资源管理的软件

2.为什么要对软硬件资源进行管理?

通过合理的管理软硬件资源(手段),为用户提供良好的(稳定的,高效的,安全的)执行环境,这是最终的目的

3.操作系统是怎样进行管理的?

注:管理的本质是对数据进行管理

  • 我们可以通过一个例子来进行理解:一个大学有上万个学生,校长是如何管理这么多的学生呢?

  • 校长也就是学生的管理者,学生就是被管理者

  • 校长并不是直接对学生面对面的进行交互式管理,而是通过学生的数据对学生进行管理

  • 那么校长是如何拿到学生的数据的呢?

  • 校长可以通过辅导员拿到学生的数据,且这个数据是不断更新的

  • 在这个例子中,校长拿到数据对数据进行决策,因此校长是决策者辅导员从学生哪里收集数据,因此辅导员是执行者

如下图所示:

image-20230128235837098

综上我们可以得出如下结论:

​ 1.校长通过对数据做管理,来对学生进行管理

​ 2.数据的采集和决策的落实,由辅导员来完成

注:由于学生的人数特别的多,那么数据也就会特别多,因此我们需要对学生的数据做管理,那么如何对数据进行管理呢?

如下图所示:

image-20230129005616956

综上我们可以得出下面的结论:

​ 1.管理的本质是对数据做管理

​ 2.管理的方法:先描述,再组织

如下图:我们介绍操作系统对于软件的管理

image-20230129172914948

下图可以更清晰的展示:

image-20230129173122952

进程

进程的基本概念

一个运行起来(加载到内存)的程序,被称为进程

注:一个可执行程序加载到内存就是一个进程,因此,进程的数量可能是及其庞大的,那么操作系统就需要对进程

来进行管理,因此也必须符合我们上述所说的先描述,再组织。

描述进程——PCB

1.进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。

2.课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

下图可以清晰的展示操作系统是如何对进程进行管理的:

image-20230129185631010

查看进程

  • ps ajx :这条指令可以将系统中所有的进程显示出来
-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
[qwy@VM-4-3-centos review]$ cat makefile
myproc:myproc.c
	gcc -o myproc myproc.c
.PHONY:clean
clean:
	rm -f myproc
-rwxrwxr-x 1 qwy qwy 8408 Jan 29 19:15 myproc
-rw-rw-r-- 1 qwy qwy  153 Jan 29 19:15 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>    // sleep()的头文件

int main()
{
    while(1)
    {
        printf("我是一个进程\n");
        sleep(1);
    }

    return 0;
}

image-20230512201328388

​ 如上图所示,当我们运行myproc这个可执行程序时,我们就可以通过ps ajx来查看这个进程,并通过grep来进

行筛选;其中grep也是一个进程,因此我们会筛选出两个进程。(进程在调度运行的时候,进程就具有动态属性)

  • ps ajx | grep 'myproc' 显示myproc这个进程的相关信息

  • ps ajx | head -1 显示进程信息所对应的标题

    具体操作如下:

[qwy@VM-4-3-centos lesson11]$ ps ajx | head -1 && ps ajx | grep 'myproc'
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 7569 15871 15871  7569 pts/0    15871 S+    1001   0:00 ./myproc
11668 15883 15882 11668 pts/1    15882 S+    1001   0:00 grep --color=auto myproc
  • 我们可以通过 kill -9 PID 来杀死对应PID的进程,操作如下图:

image-20230512201836144

系统调用:getpid()

// 查看getpid()的手册
[qwy@VM-4-3-centos review]$ man getpid

getpid()的用法:如下图所示

image-20230512203024201

// 先了解一个系统调用的接口函数 getpid()
// getpid()的头文件
 #include <sys/types.h>
 #include <unistd.h>
// getpid()的返回值
getpid() returns the process ID of the calling process. 
    
-----------------------------------------------------------------------------------------
-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
myproc:myproc.c
	gcc -o myproc myproc.c
.PHONY:clean
clean:
	rm -f myproc    
-rwxrwxr-x 1 qwy qwy 8464 Jan 29 19:54 myproc
-rw-rw-r-- 1 qwy qwy  204 Jan 29 19:54 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while(1)
    {
        printf("我是一个进程, 我的ID是:%d\n", getpid());
        sleep(1);
    }

    return 0;
}

具体演示如下图所示:

image-20230129195716468

// 由上图我们可以了解到一个新的指令  kill -l  可以查看信号

查看进程的另一种方式(了解)

// 首先,我们先介绍一个内存级的目录proc
具体操作如下:
[qwy@VM-4-3-centos lesson11]$ ls /proc/
    
// 当我们的程序在运行时就可以在 proc 这个目录中找到我们对应的进程的目录

image-20230129205910157

在proc中必然存在正在运行的程序对应的目录
    // 如果我们终止程序,则这个目录也会消失

image-20230129210255929

// 我们进入到proc中对应PID的目录我们能够发现
1.cwd:进程当前的工作路径
2.exe:进程的可执行程序的磁盘文件的路径

在这里插入图片描述

// 如果在程序运行的过程中,我们删除掉磁盘的可执行程序的文件myproc,进程是否还可以继续运行?
答:理论上,一般情况都是可以运行的;因此可执行程序已经被加载到内存中了,如下图所示

在这里插入图片描述

学习更多的系统调用接口:getppid()

image-20230515184946885

// 首先,我们先了解一个系统调用函数 getppid()
pid_t getppid(void);

// getppid()对应的头文件
 #include <sys/types.h>
 #include <unistd.h>

// getppid()的返回值
// 返回调用进程的父进程的pid
returns the process ID of the parent of the calling process.

-----------------------------------------------------------------------------------------
-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
-rwxrwxr-x 1 qwy qwy 8520 Jan 29 21:34 myproc
-rw-rw-r-- 1 qwy qwy  240 Jan 29 21:32 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

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

    return 0;
}
  • 当我们重复运行程序之后,我们可以发现,子进程的pid是一直变化的,但是父进程的pid是一直不变的
  • 这是因为命令行上启动的进程,一般情况下,它的父进程都是bash
  • 也就是说其他的进程都是由bash来创建的
  • 如果我们杀死bash,则会退出系统(kill -9 bash的pid),再次登录之后,bash的pid就会改变

image-20230515185409592

创建进程-fork

image-20230515190429492

// 首先我们先来认识创建子进程的接口函数fork()
pid_t fork(void);

// fork()的头文件
#include <unistd.h>

// fork()的返回值
  On success, the PID of the child process is returned in the parent, and 0 is returned in the child.  On failure, -1 is returned in  the  parent, no child process is created, and errno is set appropriately.
  如果成功,在父进程中返回子进程的PID,在子进程中返回0。如果失败,在父进程中返回-1,不创建子进程,并适当设置errno。
 
/*
 注:fork()执行前,只有一个父进程
    fork函数执行后,则会同时存在父进程和父进程创建出来的子进程
    且fork()后的代码,会被子进程和父进程共享
*/

首先验证fork的返回值:

-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
-rwxrwxr-x 1 qwy qwy 8512 Jan 29 22:10 myproc
-rw-rw-r-- 1 qwy qwy  434 Jan 29 22:10 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    
    // 创建子进程 -- fork是一个函数 
    // 函数执行前: 只有一个父进程
    // 函数执行后: 父进程+子进程
    pid_t id = fork();

    // 同一个变量id, 在后续不会被修改的情况下,竟然有不同的内容!
    printf("我是一个进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);

    return 0;

}

-----------------------------------------------------------------------------------------
    
[qwy@VM-4-3-centos review]$ ./myproc
    // pid_t id = fork() 在父进程中返回的id值为创建的子进程的pid
    // 父进程:pid为9667,bash为父进程的父进程:pid为7051
我是一个进程, pid: 9667, ppid: 7051, id: 9668
    
    // pid_t id = fork() 在子进程中返回的id值为0
    // 子进程:pid为9668,父进程:pid为9667
我是一个进程, pid: 9668, ppid: 9667, id: 0

// 我们可以发现返回给父进程的id为子进程的pid;返回给子进程的id为0

image-20230515191403034

// 验证父子进程会同时执行子进程创建之后的代码:
-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
-rwxrwxr-x 1 qwy qwy 8568 Jan 29 22:14 myproc
-rw-rw-r-- 1 qwy qwy  657 Jan 29 22:14 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{  
    // 创建子进程 -- fork是一个函数 
    // 函数执行前: 只有一个父进程
    // 函数执行后: 父进程+子进程
    pid_t id = fork();
    if(id == 0)
    {
        // 当id == 0时,说明为子进程
        while(1)
        {
            printf("子进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);
            sleep(1);
        }
    }
    else if(id > 0)
    {
        // 此处为父进程
        while(1)
        {
            printf("父进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);
            sleep(2);
        }
    }
    else
    {

    }

    return 0;
}

/*
 注:fork()执行前,只有一个父进程
    1.fork函数执行后,则会同时存在父进程和父进程创建出来的子进程
    2.fork()后的代码,会被子进程和父进程共享
    3.通过返回值的不同,让父子进程执行后续共享代码的一部分
*/

image-20230515193025879

进程状态

1.什么是运行?(进程的运行状态)

​ 在程序加载到内存中之后,会有对应的进程控制块(PCB)对进程进行描述,然后再将进程控制块(PCB)组

织起来,如果我们想要运行这些进程,就需要将这些进程对应的task_struct(PCB)结构体对象放入到runqueue

(运行队列,用数据结构的方法将其组织起来),然后依次或者轮转将其放入CPU进行计算,而只要我们将进程对

应的task_struct结构体对象放入到runqueue(运行队列)就代表这个进程是运行状态,且对应的task_struct

(PCB)中会显示其进程状态为运行状态。

注:

  1. 一个CPU只有一个runqueue(运行队列)
  2. 并不止是进程正在CPU中正在进行计算才是运行状态 ,只要进程在运行队列中,就是运行状态。
  3. 进程的状态,是进程内部的属性,且都存放在task_struct中,内部可能用一个整型来代表进程的不同状态,如int(1:表示运行状态,2:表示阻塞状态)

image-20230515212944676

2.什么是阻塞?(进程的阻塞状态)

注:

  1. 进程不只是会占用CPU的资源,也可能随时随地的要用到外设资源(比如磁盘,要在磁盘读取数据或者写入数据)

  2. 所谓的进程的不同状态,本质是进程在不同的队列中,等待某种资源。

  3. 将进程放入队列,其实就是将进程对应的task_struct结构体对象放入到队列

    比如:当进程在CPU中计算时,发现需要访问外设(类似磁盘),那么我们就将进程从CPU的运行队列,放入到

磁盘的等待队列(waitqueue,也可能有很多的进程都需要访问磁盘,因此需要一个等待队列来对这些进程进行组

织),进程放入到等待队列后,进程对应的task_struct结构体对象中的运行状态也就会改变为阻塞状态,当进程访

问完磁盘之后,则又会将阻塞状态改为运行状态,并将进程重新放入到CPU的运行队列,等待CPU来进行运算。

image-20230515215510977

3.什么是挂起?(进程的挂起状态)

   当大量的进程都为阻塞状态时,进程的代码和数据都会占据内存空间,且PCB的结构体对象也会占据内存空间,但是又有源源不断的进程加载到内存中,那么如果内存满了,那该怎么办?
       // 此时,操作系统会将阻塞状态的进程的task_struct结构体对象指向的代码和数据,从内存中剪切到磁盘中,此时进程的状态就会变为挂起状态,同时也节省了内存空间,那么我们就可以加载新的进程;

Linux内核源代码的定义

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 */
};

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    
S睡眠状态(sleeping)(这里的s状态时阻塞状态的一种): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
    
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
    
T停止状态(stopped)(暂停状态): 可以通过发送 SIGSTOP(signal stop) 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT (signal continue)信号让进程继续运行。
    
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

演示

R(运行状态)

// 我们先来运行一段代码
-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
-rwxrwxr-x 1 qwy qwy 8312 Jan 30 18:35 myproc
-rw-rw-r-- 1 qwy qwy   99 Jan 30 18:35 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>

int main()
{
    while(1)
    {
       int a = 10 + 20;
    }

    return 0;
}
[qwy@VM-4-3-centos lesson11]$ ./myproc
    
// 编译运行之后,我们使用另一终端来观察这个进程的状态
  
-----------------------------------------------------------------------------------------
[qwy@VM-4-3-centos lesson11]$ ps ajx | head -1 && ps ajx | grep myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
23065 25301 25301 23065 pts/0    25301 R+    1001   2:00 ./myproc
24984 25713 25712 24984 pts/1    25712 R+    1001   0:00 grep --color=auto myproc

// STAT(status)表示当前程序的状态
// 由上方打印到终端上的信息,我们可以清晰的观察到是R状态(也就是运行状态)

image-20230515230323066

S(睡眠状态)

-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
-rwxrwxr-x 1 qwy qwy 8360 Jan 30 18:49 myproc
-rw-rw-r-- 1 qwy qwy  200 Jan 30 18:49 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>

int main()
{
    int count = 0;
    int a = 0;
    while(1)
    {
       a = 10 + 20;
       printf("当前a的值是:%d, runing flag: %d\n", a, count++);
    }

    return 0;
}
[qwy@VM-4-3-centos lesson11]$ ./myproc

-----------------------------------------------------------------------------------------
    
    // 当我们运行程序之后,会发现当前程序是S状态,这是为什么呢?
    // 这是由于printf()是将数据打印到显示器中,但是显示器属于外设的一种,它的速度是非常慢的,而我们CPU计算的速度相对于外设是特别快的,因此这个进程99%的时间都是等待IO就绪(也就是等待显示器,或者其他外设就绪,即进程在外设的等待队列中),只有1%的时间来执行打印代码,因此,系统就会将当前程序的状态标识为S状态(睡眠状态,也叫作浅度睡眠)
    
    注:S状态也是阻塞状态的一种

image-20230130185131918

T(暂停状态)

// 暂停状态也是阻塞的一种,但是有没有被挂起是不清楚的,这是由操作系统决定的。
// 暂停状态,是通过命令来让当前进程暂停的
// kill -l 查看信号列表
[qwy@VM-4-3-centos lesson11]$ kill -l
// 由kill -l 查询得知 18) SIGCONT	19) SIGSTOP   
// 18可以使暂停状态的进程继续运行   19可以使运行状态的进程暂停,变为暂停状态
    
=========================================================================================
    
// 演示
// 首先我们先运行一段代码
-rw-rw-r-- 1 qwy qwy   75 Jan 29 19:15 makefile
-rwxrwxr-x 1 qwy qwy 8408 Jan 30 19:03 myproc
-rw-rw-r-- 1 qwy qwy  237 Jan 30 19:02 myproc.c
[qwy@VM-4-3-centos lesson11]$ cat myproc.c
#include <stdio.h>

int main()
{
    int a = 0;
    while(1)
    {
       a = 10 + 20;
    }

    return 0;
}
[qwy@VM-4-3-centos lesson11]$ ./myproc
    
=========================================================================================
// 将代码运行之后,我们在另一个终端来进行接下来的操作
[qwy@VM-4-3-centos lesson11]$ ps ajx | head -1 && ps ajx | grep myproc

image-20230516111903244

// 此时进程为R+状态
// R+ 其中 + 表示进程为前台进程,前台进程一旦运行,则命令行无法获得命令行解析了(即类似于 ls pwd touch 等命令,无法解析)
[qwy@VM-4-3-centos review]$ ps ajx | head -1 && ps ajx | grep myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 7051 30409 30409  7051 pts/0    30409 R+    1001   0:30 ./myproc
10552 30562 30561 10552 pts/1    30561 R+    1001   0:00 grep --color=auto myproc

========================================================================================  
// 使用命令kill -19 5090 使程序暂停,我们就可以看到进程当前的状态为T状态
[qwy@VM-4-3-centos review]$ kill -19 5090
[qwy@VM-4-3-centos review]$ ps ajx | head -1 && ps ajx | grep myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 7051  5090  5090  7051 pts/0     7051 T     1001   0:29 ./myproc
10552  5293  5292 10552 pts/1     5292 S+    1001   0:00 grep --color=auto myproc
    
// 使用命令kill -18 5090 使程序继续运行,我们就可以看到进程当前的状态恢复为R状态
// 如果R状态后没有 + ,则表示当前进程为后台进程,命令行依旧可以进行命令行解析(即类似于 ls pwd touch 等命令),但是我们无法通过(ctrl + c)来使进程终止,只能通过 kill -9 PID 的命令来中止进程
// 如下图所示:
[qwy@VM-4-3-centos review]$ kill -18 5090
[qwy@VM-4-3-centos review]$ ps ajx | head -1 && ps ajx | grep myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 7051  5090  5090  7051 pts/0     7051 R     1001   0:31 ./myproc
10552  5347  5346 10552 pts/1     5346 R+    1001   0:00 grep --color=auto myproc

image-20230516114328458

D(磁盘睡眠)

// 我们所说的S状态是一种浅度睡眠,进程如果是浅度睡眠是可以被终止的
// 而D状态是一种深度睡眠,进程如果是深度睡眠,是不可以被终止的,在该状态的进程,无法被OS杀掉,只能通过断电,或者进程的IO完成,自己醒来,来解决。

// D(磁盘睡眠)是什么?
  如果一个进程需要向磁盘写入的数据特别大(比如20个G),那么这个进程就会变为S状态来进行数据的写入(写入数据是由磁盘来完成的,进程此时就是等待磁盘完成写入),但是如果此时内存中存在太多的进程,那么OS会将这个等待的进程杀掉(因为这个进程此时什么都没有做),而此时如果磁盘写入时出错,又无法找到这个进程来反映错误,那么磁盘就不知道该怎样来进行处理了。
  所以我们将这种需要大量数据写入的进程设置为磁盘睡眠状态,当进程等待磁盘写入时,不允许OS将磁盘睡眠状态中的进程杀掉,只能通过断电,或者进程的IO完成,自己醒来,来解决。
  
// 注: 可以通过 man dd 的指令来查询,如何进入磁盘睡眠状态

t (tracing stop)

// t (tracing stop):表示当前进程正在被追踪

// 演示
// 先运行一段代码
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9384 Jan 30 19:59 myprocess
-rw-rw-r-- 1 qwy qwy  369 Jan 30 19:59 myprocess.c
[qwy@VM-4-3-centos lesson13]$ cat makefile
myprocess:myprocess.c
	gcc -o $@ $^ -g     // $@ 表示目标文件, $^ 表示依赖文件列表, -g 让这个程序可以进行调试
.PHONY:clean
clean:
	rm -f myprocess
[qwy@VM-4-3-centos lesson13]$ cat myprocess.c
#include <stdio.h>

int main()
{
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");
    printf("hello debug\n");

    return 0;
}
[qwy@VM-4-3-centos lesson13]$ gdb myprocess

// 通过gdb调试代码,运行到断点处,再通过另一个终端来查看当前进程的状态
// 具体如下图所示

在这里插入图片描述

Z (zombie,僵尸状态)

1.什么是僵尸状态?
    当一个进程被它的父进程创建出来,这个进程是为了完成某个任务,那么当这个进程运行完之后,我们需要知道这个任务它完成的怎么样了,所以当进程退出的时候,这个程序的执行结果还有其他信息会保存到PCB中,因此我们不可以立即释放该进程对应的资源(PCB进程控制块),应该保存一段时间,让父进程或者OS来进行读取PCB。这样我们才可以了解到这个进程完成的怎么样了,读取完PCB中的信息之后,父进程就会将PCB占用的资源进行回收。
    
    所谓的僵尸状态,就是进程退出之后,等待父进程回收自身PCB资源的状态。
    注:一个进程退出,它的代码和数据是可以被释放的,但是记录这个进程信息的PCB是不能够被释放的,如果这个PCB资源一直没有被父进程或者OS回收,那么就会造成内存泄漏。

僵尸进程

// 首先创建一个子进程,让父进程不要退出,且什么都不要去做,并让子进程正常退出
// 并在另一个终端实时检测父子进程的状态

// 根据我们的要求写的一段程序
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9808 Jan 30 21:27 myprocess
-rw-rw-r-- 1 qwy qwy  506 Jan 30 21:27 myprocess.c
[qwy@VM-4-3-centos lesson13]$ cat myprocess.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {  
        //child
        while(1)
        {
            printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(5);
            exit(1);
        }
    }
    else
    {
        //parent
        while(1)
        {
            printf("I am parent proceass, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }

    return 0;
}
[qwy@VM-4-3-centos lesson13]$ ./myprocess
    
=========================================================================================
    
// 运行这段程序,并在另一个终端来监视这个进程的状态
[qwy@VM-4-3-centos lesson13]$ while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep; sleep 1; done
    
// 具体如下图所示:

image-20230130214359572

孤儿进程

// 当一个进程被创建,如果这个进程还没有退出,但是它的父进程先退出了,这也就意味着,当这个进程退出,变为僵尸状态后,因为没有父进程来回收它的PCB资源了,将这种进程被称作孤儿进程。

// 1.孤儿进程会被操作系统领养(也就是会被1号进程领养)
// 如果操作系统没有领养孤儿进程,那么这个孤儿进程对应的PCB资源也就无法被回收,会造成内存泄漏
// 可以通过下方的指令查看到1号进程,所谓的1号进程也就是操作系统
[qwy@VM-4-3-centos lesson13]$ ps ajx | head -1 && ps ajx | grep systemd

// 2.如果是前台进程创建的子进程,如果变为了孤儿进程,会自动变为后台进程

-----------------------------------------------------------------------------------------
// 演示

// 第一步:运行如下的代码
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9776 Jan 30 22:44 myprocess
-rw-rw-r-- 1 qwy qwy  485 Jan 30 22:44 myprocess.c
[qwy@VM-4-3-centos lesson13]$ cat myprocess.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {  
        //child
        while(1)
        {
            printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        //parent
        while(1)
        {
            printf("I am parent proceass, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }

    return 0;
}
[qwy@VM-4-3-centos lesson13]$ ./myprocess
  
=========================================================================================
    
// 在另一个终端实时的监视这个进程的状态,通过如下指令
[qwy@VM-4-3-centos lesson13]$ while :; do ps ajx | head -1 && ps ajx | grep process | grep -v grep; sleep 1; done

    
// 再切换一个终端,我们直接将父进程杀掉,就可以看到子进程被操作系统(1号进程领养),且变为后台程序,只能通过 kill -9 来杀掉。

// 具体操作如下:
  • 图1:未杀死父进程之前

在这里插入图片描述

  • 图二:杀死父进程之后,就可以看到子进程被操作系统(1号进程领养),且变为后台程序,只能通过 kill -9 来杀掉

在这里插入图片描述

进程优先级(了解)

1.什么是进程优先级?
    就是进程在占用资源时(CPU资源,或者其他资源),谁先占用
2.为什么会存在优先级?
    因为系统的资源很少,但是进程特别多,因此有了进程的优先级
3.Linux优先级
    优先级的本质就是PCB中的一个整数数字(也可能是好几个整数数字)
    
// 我们可以使用命令 ps -la 来查看进程的优先级
[qwy@VM-4-3-centos lesson13]$ ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 R  1001 32687 32571  0  80   0 - 38324 -      pts/3    00:00:00 ps
   
// 最终优先级 = PRI + NI    其中PRI恒等于80
// PRI -> priority   NI -> nice
// Linux支持进程运行中,进行优先级调整,调整的策略就是更改nice来完成
// Linux不允许进程无节制的设置优先级,nice其取值范围是[-20,19]共40个级别,可以自己设置范围以外的数来验证
    
    注:最终优先级的值越小代表优先级越高

修改进程的优先级

修改进程的优先级,必须用root权限
// 第一步:我们自己先运行一段代码,这样才可以查看到对应的进程,才可以修改优先级
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9456 Jan 31 00:48 myprocess
-rw-rw-r-- 1 qwy qwy  140 Jan 31 00:48 myprocess.c
[qwy@VM-4-3-centos lesson13]$ cat myprocess.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    while(1)
     printf(" pid: %d\n", getpid());

    return 0;
}

// 第二步:在另一个终端查看一下当前进程的优先级
[qwy@VM-4-3-centos lesson13]$ ps -al
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1001  4256 32571  6  80   0 -  1054 n_tty_ pts/3    00:00:00 myproces
0 R  1001  4275  2004  0  80   0 - 38332 -      pts/4    00:00:00 ps
    
// 第三步:在root权限下使用top命令去修改NI(也就是去修改nice值)
[qwy@VM-4-3-centos lesson13]$ sudo top

// 第四步:
    输入r
    // 屏幕会打印  PID to renice [default pid = 4256]
    再输入要修改进程的PID
    // 屏幕会打印 Renice PID 4256 to value
    再输入nice值
    // 修改完成
    
    再次查看进程的优先级,我们发现进程的优先级已经被修改了
[qwy@VM-4-3-centos lesson13]$ ps -al
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1001  4256 32571  6  90  10 -  1054 n_tty_ pts/3    00:00:28 myproces
0 R  1001  5761  2004  0  80   0 - 38332 -      pts/4    00:00:00 ps

其他概念

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

进程切换

// CPU中存在大量的寄存器,当我们的进程在运行时,CPU会根据我们进程的PCB(task_astruct)来找到对应的代码和数据,进而开始取指令、分析指令、执行指令,来进行运算,运算会产生一些临时数据在寄存器当中,这份数据是属于当前正在运行的进程的。

// 但是每个进程在运行的时候,占用CPU,并不是一直占用,直到进程运算结束,而是都有属于自己的时间片(可能这个进程运算10纳秒就切换其他进程了)。

// 进程在切换的时候,要进行进程的上下文保护(所谓的上下文保护,也就是保护当前进程运算多的数据,如:产生的临时数据,代码运行到哪一行了,等等);  我们可以将上下文保护理解为将上下文数据保存到了PCB当中

// 等到进程再次恢复运行时,要进行上下文数据恢复

注:CPU中有一个寄存器 pc/eip 是用来存储当前正在执行指令的下一条指令的地址;所以当恢复上下文数据后,就可以接着上次的运行接着运算了

环境变量

基本概念

1.环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数

2.环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

常见环境变量

PATH : 指定命令的搜索路径

HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)

查看环境变量方法

echo $NAME   //NAME:你要查看的环境变量名称

测试PATH

// 1.我们自己写一个程序,如果我们想要去运行,就必须加上程序所在路径,找到这个程序,才可以运行这个程序,如下所示
// ./myprocess  表示当前目录下的程序myprocess
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9448 Jan 31 16:15 myprocess
-rw-rw-r-- 1 qwy qwy  142 Jan 31 16:14 myprocess.c
[qwy@VM-4-3-centos lesson13]$ ./myprocess     
 pid: 23035

=========================================================================================
     
// 2.但是我们发现Linux中的指令,像ls,pwd,touch等也是可执行程序,那么为什么这些可执行程序不需要指定路径就可以运行呢?
   // 这是因为系统已经默认帮这些可执行程序指定了路径(都是在 /usr/bin/ 系统的默认路径下)
   
   // 如果我们想要我们的程序也不需要加上程序所在路径就可以直接运行,只需要将我们的程序,移动到/usr/bin/ 系统的默认路径下就可以了(移动到 /usr/bin/ 需要root权限),演示如下:
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9448 Jan 31 16:15 myprocess
-rw-rw-r-- 1 qwy qwy  142 Jan 31 16:14 myprocess.c
[qwy@VM-4-3-centos lesson13]$ sudo cp myprocess /usr/bin/
[qwy@VM-4-3-centos lesson13]$ myprocess          // 此时就可以直接运行,不需要加路径
 pid: 25914
[qwy@VM-4-3-centos lesson13]$ sudo rm /usr/bin/myprocess 
 // 但是记得删除,因为我们的程序会污染指令池

     =========================================================================================
     
// 3.那么为什么放在 /usr/bin/ 路径下,就不需要添加路径,就可以执行程序呢?
   这是因为PATH是操作系统启动时定义的一个环境变量,且是全局有效的
     
// 查看PATH
[qwy@VM-4-3-centos lesson13]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/qwy/.local/bin:/home/qwy/bin
// 其中 :为分隔符,指令在运行时,系统会默认在这些路径下进行查询,只要在对应路径下找到指令,那么就会运行
    
// 因此如果我们想要我们的程序不需要加上对应路径,我们只需要将对应的路径添加到PATH中
// 演示如下:
-rw-rw-r-- 1 qwy qwy   77 Jan 30 19:57 makefile
-rwxrwxr-x 1 qwy qwy 9448 Jan 31 16:15 myprocess
-rw-rw-r-- 1 qwy qwy  142 Jan 31 16:14 myprocess.c
[qwy@VM-4-3-centos lesson13]$ pwd
/home/qwy/lesson13
[qwy@VM-4-3-centos lesson13]$ export PATH=$PATH:/home/qwy/lesson13  // 注意格式
[qwy@VM-4-3-centos lesson13]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/qwy/.local/bin:/home/qwy/bin:/home/qwy/lesson13
[qwy@VM-4-3-centos lesson13]$ myprocess
 pid: 31132
  
=========================================================================================    
// 我们只要重启Xshell,我们对PATH添加的路径就会消失,这是因为每一次启动时,系统会默认添加配置文件中的环境变量(根据.bash_profile和.bashrc)
[qwy@VM-4-3-centos ~]$ ls -al
-rw-r--r--   1 qwy  qwy    193 Apr  1  2020 .bash_profile
-rw-r--r--   1 qwy  qwy    351 Dec 29 01:19 .bashrc

测试HOME

[qwy@VM-4-3-centos lesson13]$ echo $HOME
/home/qwy
[qwy@VM-4-3-centos lesson13]$ cd ~   // 也就是进入HOME对应的路径

// 通过env指令就可以查看所有的环境变量   
[qwy@VM-4-3-centos lesson13]$ env

获取环境变量getenv()

image-20230516155323037

// getenv() -get an environment variable
// 头文件和函数调用接口
#include <stdlib.h>
char *getenv(const char *name);      

// 返回值
The getenv() function returns a pointer to the value in the environment, or NULL if there is no match.
    函数返回一个指向环境中的值的指针,如果没有匹配,则返回NULL。
    
The  getenv()  function searches the environment list to find the environment variable name, and returns a pointer to the corresponding value string.
    getenv()函数在环境列表中搜索环境变量名,并返回一个指向相应值字符串的指针
    
// 参数
const char *name  就是所要搜索的环境变量的名字
 
=========================================================================================      
// 演示
-rw-rw-r-- 1 qwy qwy   70 Jan 31 17:38 makefile
-rwxrwxr-x 1 qwy qwy 8408 Jan 31 17:41 mycmd
-rw-rw-r-- 1 qwy qwy  152 Jan 31 17:41 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>
#include <stdlib.h>

#define USER "USER"

int main()
{
    char* who = getenv(USER);

    printf("user: %s\n", who);
    return 0;
}    
[qwy@VM-4-3-centos lesson14]$ ./mycmd
user: qwy
[qwy@VM-4-3-centos lesson14]$ echo $USER
qwy

========================================================================================= 
    
// USER环境变量最大的意义就是可以标识当前使用Linux的用户
// 通过下面的演示,我们就可以发现,判断当前使用用户,就可以对当前用户的使用权限进行限制
-rw-rw-r-- 1 qwy qwy   70 Jan 31 17:38 makefile
-rwxrwxr-x 1 qwy qwy 8512 Jan 31 17:51 mycmd
-rw-rw-r-- 1 qwy qwy  378 Jan 31 17:51 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define USER "USER"

int main()
{
    char* who = getenv(USER);
    if(strcmp(who, "root") == 0)
    {
        // root权限才可以运行到此处
    	printf("user: %s\n", who);
    	printf("user: %s\n", who);
   	 	printf("user: %s\n", who);
   	 	printf("user: %s\n", who);
   	 	printf("user: %s\n", who);
    }
    else
    {
        // 非root权限才可以运行到这里
        printf("权限不足!\n");
    }
    return 0;
}
[qwy@VM-4-3-centos lesson14]$ echo $USER
qwy
[qwy@VM-4-3-centos lesson14]$ ./mycmd
权限不足! 
[qwy@VM-4-3-centos lesson14]$ sudo ./mycmd
[sudo] password for qwy: 
user: root
user: root
user: root
user: root
user: root

增加/删除环境变量

1.增加本地变量,本地变量只会在当前进程(bash)内有效
    
// 演示1:
[qwy@VM-4-3-centos lesson14]$ myenv=1234    // 增加本地变量 myenv=1234 
[qwy@VM-4-3-centos lesson14]$ echo $myenv
1234
[qwy@VM-4-3-centos lesson14]$ env | grep myenv 
[qwy@VM-4-3-centos lesson14]$ 
 // 在全局的环境变量中是无法查找到我们定义的本地变量的(也就是env中查找不到)
    
// 演示2:写一个程序来获取本地变量myenv,发现无法获取,为什么呢?
// 这是因为bash就是一个系统进程,mycmd运行后也会变为一个进程,bash的子进程
// 本地变量只会在当前进程(bash)有效,并不会被子进程mycmd继承,因此无法获取本地变量myenv
// 如下进行了验证:
-rw-rw-r-- 1 qwy qwy   70 Jan 31 17:38 makefile
-rwxrwxr-x 1 qwy qwy 8408 Jan 31 18:29 mycmd
-rw-rw-r-- 1 qwy qwy  243 Jan 31 18:29 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>
#include <stdlib.h>

#define MY_ENV "myenv"

int main()
{
    char* myenv = getenv(MY_ENV);
    if(NULL == myenv)
    {
        printf("%s is not found\n", MY_ENV);
    }
    printf("%s=%s\n", MY_ENV, myenv);

    return 0;
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
myenv is not found
myenv=(null)
    
========================================================================================= 
    
2.全局的环境变量:在bash内有效,也可以被继承
// 演示3:
// 如果我们将本地变量myenv设置为全局的环境变量,那么我们myenv就可以被子进程继承
// 通过export来将本地变量myenv设置为全局的环境变量
// 此时就可以通过我们的程序来获取
[qwy@VM-4-3-centos lesson14]$ export myenv
[qwy@VM-4-3-centos lesson14]$ env | grep myenv
myenv=1234
[qwy@VM-4-3-centos lesson14]$ ./mycmd
myenv=1234

  
// 演示4:
// unset: 清除环境变量
// 清除我们设置的全局环境变量myenv
[qwy@VM-4-3-centos lesson14]$ unset myenv
[qwy@VM-4-3-centos lesson14]$ env | grep myenv   // 清除后,查询不到了
[qwy@VM-4-3-centos lesson14]$ echo $myenv
[qwy@VM-4-3-centos lesson14]$

// 演示5:
// set: 显示本地定义的shell变量和环境变量
// 重新设置一个本地变量yourenv
[qwy@VM-4-3-centos lesson14]$ yourenv=5678
[qwy@VM-4-3-centos lesson14]$ echo $yourenv
5678
[qwy@VM-4-3-centos lesson14]$ set | grep yourenv   // 可以同时查到本地变量和全局的环境变量
yourenv=5678
[qwy@VM-4-3-centos lesson14]$ env | grep yourenv   // 使用env是查询不到本地变量的

// 用export直接设置全局的环境变量
[qwy@VM-4-3-centos lesson14]$ export MYENV="youcanseeme"
[qwy@VM-4-3-centos lesson14]$ env | grep MYENV
MYENV=youcanseeme

测试PWD

// PWD这个环境变量是用来记录我们所在的当前路径的
[qwy@VM-4-3-centos lesson14]$ echo $PWD
/home/qwy/lesson14
    
// 我们也可以写一个程序来进行演示
-rw-rw-r-- 1 qwy qwy   70 Jan 31 17:38 makefile
-rwxrwxr-x 1 qwy qwy 8408 Jan 31 20:16 mycmd
-rw-rw-r-- 1 qwy qwy  121 Jan 31 20:16 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>
#include <stdlib.h>

#define PWD "PWD"

int main()
{
    printf("%s\n", getenv(PWD));
    return 0;
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
/home/qwy/lesson14

main函数的参数

1.main()有参数吗?
    答案是肯定有的
    
// main()函数的前两个参数
// 注:main函数是由系统来调用的,main函数的参数是由系统来传参的
int main(int argc, char* argv[])
{                                                                                             return 0;                                                                              }  
// argc 是 argument count的缩写;   argv 是 argument vector的缩写
// argc 是命令行参数的个数,argv[]是储存命令行参数的指针数组(一般储存的都是字符串)

// 演示:系统将命令行参数传递给了main函数
-rw-rw-r-- 1 qwy qwy   79 Jan 31 20:57 makefile
-rwxrwxr-x 1 qwy qwy 8360 Jan 31 20:58 mycmd
-rw-rw-r-- 1 qwy qwy  166 Jan 31 20:58 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>

int main(int argc, char* argv[])
{
    for(int i = 0; i < argc; i++)
    {
        printf("argv[%d] -> %s\n", i, argv[i]);
    }

    return 0;
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
argv[0] -> ./mycmd
[qwy@VM-4-3-centos lesson14]$ ./mycmd -a
argv[0] -> ./mycmd
argv[1] -> -a
[qwy@VM-4-3-centos lesson14]$ ./mycmd -a -b
argv[0] -> ./mycmd
argv[1] -> -a
argv[2] -> -b
    
========================================================================================= 
    
[qwy@VM-4-3-centos lesson14]$ ./mycmd -a -b

// 与上面的进行对比,(./mycmd 和 ls 都是可执行程序,后面跟着的都是选项)
// 我们可以判断出Linux中的命令和选项也是这么来写的
[qwy@VM-4-3-centos lesson14]$ ls -a -l -d
drwxrwxr-x 2 qwy qwy 4096 Jan 31 20:58 .
    
========================================================================================= 

// 我们向命令行输入 ls -a -b -c -d -e 是一个长字符串
// 命令行在进行解析的时候,会将其用空格为分隔符,分离成若干字符串 如:"ls" "-a" "-b" "-c" "-d" "-e"
// 再依次将其存储到指针数组argv[]中,(这些都是shell和系统来做的)
// argv[0] -> ls
// argv[1] -> -a
// argv[2] -> -b
// argv[3] -> -c
// argv[4] -> -d
// argv[5] -> -e
    
    
// 应用演示
-rw-rw-r-- 1 qwy qwy   79 Jan 31 20:57 makefile
-rwxrwxr-x 1 qwy qwy 8464 Jan 31 21:30 mycmd
-rw-rw-r-- 1 qwy qwy  422 Jan 31 21:30 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        // 也可以进行多样组合,如:ab,abc bc 等等
        // \n\t 换行符+制表位
        printf("Usage: \n\t%s [-a/-b/-c]\n", argv[0]); 
        return 1;
    }
    if(strcmp("-a", argv[1]) == 0)
    {
        printf("功能a\n");
    }
    if(strcmp("-b", argv[1]) == 0)
    {
        printf("功能b\n");
    }
    if(strcmp("-c", argv[1]) == 0)
    {
        printf("功能c\n");
    }
    return 0;
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
Usage: 
	./mycmd [-a/-b/-c]
[qwy@VM-4-3-centos lesson14]$ ./mycmd -a   // 不同选项来实现不同的功能
功能a
[qwy@VM-4-3-centos lesson14]$ ./mycmd -b
功能b
[qwy@VM-4-3-centos lesson14]$ ./mycmd -c
功能c

main函数的第三个参数(获取环境变量)

// main函数的第三个参数,是环境变量的参数
int main(int argc, char *argv[], char *env[])
{
    
    return 0;
}
// char *env[] 也是一个指针数组,如下图所示

image-20230131214024763

// 运行下面的程序我们就可以打印出环境变量
-rw-rw-r-- 1 qwy qwy   79 Jan 31 20:57 makefile
-rwxrwxr-x 1 qwy qwy 8360 Jan 31 21:49 mycmd
-rw-rw-r-- 1 qwy qwy  172 Jan 31 21:49 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>

int main(int argc, char* argv[], char *env[])
{
    for(int i = 0; env[i]; i++)
    {
        printf("env[%d]->%s\n", i, env[i]);
    }
    return 0;
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
env[0]->XDG_SESSION_ID=305614
env[1]->HOSTNAME=VM-4-3-centos
env[2]->TERM=xterm
env[3]->SHELL=/bin/bash
env[4]->HISTSIZE=3000
env[5]->SSH_CLIENT=1.86.62.163 34215 22
env[6]->SSH_TTY=/dev/pts/1
env[7]->USER=qwy
.............................等等

使用environ来获取环境变量

image-20230131214024763

// 二级指针 char **environ,它是指向 char *env[]
// *(environ + 0) 指向 env[0] 
// *(environ + 1) 指向 env[1] 
..............

// 代码演示
-rw-rw-r-- 1 qwy qwy   79 Jan 31 20:57 makefile
-rwxrwxr-x 1 qwy qwy 8432 Jan 31 22:07 mycmd
-rw-rw-r-- 1 qwy qwy  169 Jan 31 22:07 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>

int main()
{
 	extern char** environ;   // extern 外部变量声明
    for(int i = 0;environ[i];i++)   // 当environ[i]指向NULL时,循环结束
 	{
   		printf("env[%d]->%s\n",i, environ[i]);
 	}
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
env[0]->XDG_SESSION_ID=305614
env[1]->HOSTNAME=VM-4-3-centos
env[2]->TERM=xterm
env[3]->SHELL=/bin/bash
env[4]->HISTSIZE=3000
env[5]->SSH_CLIENT=1.86.62.163 34215 22
env[6]->SSH_TTY=/dev/pts/1
env[7]->USER=qwy
...................等等

获取环境变量三种方式的总结

// 第一种方式:通过函数getenv()
1.char *getenv(const char *name);  

// 第二种方式:通过main函数的第三个参数,char *env[]
2.char *env[]
    
// 第三种方式:二级指针 char **environ
3.extern char** environ

程序地址空间

程序地址空间回顾

image-20230131225304060

提出的一个问题

// 一个32位平台,所能控制的字节量就是2^32个bit,也就是4G空间,如上图的各个数据段就分布到这4G的空间上。
// 那么我们曾经学的进程地址空间,是内存吗?

// 我们先来看一段程序
-rw-rw-r-- 1 qwy qwy   80 Jan 31 23:16 makefile
-rwxrwxr-x 1 qwy qwy 8656 Jan 31 23:16 mycmd
-rw-rw-r-- 1 qwy qwy  873 Jan 31 23:15 mycmd.c
[qwy@VM-4-3-centos lesson14]$ cat mycmd.c
#include <stdio.h>
#include <unistd.h>

// 创建一个全局变量
int global_value = 100;

int main()
{
    // 创建一个子进程
    pid_t id = fork();
    if(id < 0)
    {
        // 不是父进程,也不是子进程,运行到这里,打印错误信息
        printf("fork error\n");
        return 1;
    }
    else if(id == 0)
    {
        // 子进程可以运行到这里
        int cnt = 0;
        while(1)
        {
            printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
            sleep(1);
            cnt++;
            if(cnt == 6)
            {
                global_value = 300;
                printf("子进程已经更改了全局的变量啦..........\n");
            }
        }
    }
    else
    {
        // 父进程会运行到这里
        while(1)
        {
            printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
            sleep(2);
        }
    }
    sleep(1);
}
[qwy@VM-4-3-centos lesson14]$ ./mycmd
我是父进程, pid: 10675, ppid: 5173 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 100, &global_value: 0x60105c
我是父进程, pid: 10675, ppid: 5173 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 100, &global_value: 0x60105c
我是父进程, pid: 10675, ppid: 5173 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 100, &global_value: 0x60105c
我是父进程, pid: 10675, ppid: 5173 | global_value: 100, &global_value: 0x60105c
子进程已经更改了全局的变量啦..........
我是子进程, pid: 10676, ppid: 10675 | global_value: 300, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 300, &global_value: 0x60105c
我是父进程, pid: 10675, ppid: 5173 | global_value: 100, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 300, &global_value: 0x60105c
我是子进程, pid: 10676, ppid: 10675 | global_value: 300, &global_value: 0x60105c
我是父进程, pid: 10675, ppid: 5173 | global_value: 100, &global_value: 0x60105c

// 我们发现子进程的值已经被改变了,父进程和子进程的值已经不一致了,但是地址却是一样的
    
    // 多进程在读取同一块地址的时候,怎么可能出现不同的结果?
    这是因为这里的地址,绝对不是物理地址,而是虚拟地址(线性地址,也叫逻辑地址);我们曾经学习语言的基本地址(指针),不是对应的物理地址,而是虚拟地址。
    // 具体后面再解答
    
    // 感性的理解虚拟地址空间
    每一个进程都认为自己是独占系统资源的(事实上并不是)

进程地址空间

1.什么是进程地址空间呢?

​ 进程地址空间其实就是操作系统为进程画的一张大饼(就是让每一个进程都认为整个内存空间都是自己的,所

有的资源都是自己独享的),所以每个进程都有自己的进程地址空间(且进程认为自己的进程地址空间就是整个内存空间的大小)。

注:

一个进程是不会一下子申请所有的内存的,如果申请的内存空间过大,或者是操作系统的内存不够了,那么操作系

统会拒绝申请的,所以才是大饼,让进程以为自己拥有所有的资源,其实并不是。

2.由于每个进程都有自己的进程地址空间,所以操作系统需要对这些进程地址空间进行管理,那么该怎样管理呢?

​ 还是采用先描述,再组织的方法。

​ 描述:

​ 将对进程地址空间的描述,放入 mm_struct 结构体中

​ 组织:

​ 将所有进程地址空间的 mm_struct对象用数据结构组织起来

理解进程地址空间

1.进程地址空间描述的基本空间大小是字节

2.在32位下,有2^32个字节

3.每一个字节,对应一个地址因此有4GB空间范围

4.为了保证每一个字节都有唯一的地址,我们用32位2进制数来表示每一个字节空间的地址

  • 如下图所示

image-20230201001923424

// 由上图我们可以分析出,我们可以利用地址来对进程地址看空间的代码区(code)、数据区(data)、堆区(heap)、栈区(stack)进行划分

// 用 mm_struct 来描述就是
struct mm_struct
{
    // 代码区
    unit32_t code_start;
    unit32_t code_end;
    // 数据区
    unit32_t data_start;
    unit32_t data_end;
    // 堆区
    unit32_t heap_start;
    unit32_t heap_end;
    // 栈区
    unit32_t stack_start;
    unit32_t stack_end;
}

// 创建一个进程地址空间的对象(这个对象的地址是在task_struct中,也就是PCB中)
// 里面的地址都是我虚构的,方便大家理解,不要较真
int main()
{
    *mm = malloc(struct mm_struct);
    mm->code_start = 0x1111 1111     // 代码区
    mm->code_end = 0x1211 1101
    mm->data_start = 0x1211 1110     // 数据区
    mm->data_end = 0x1400 0000
    mm->heap_start = 0x1400 0001     // 堆区
    mm->heap_end = 0x1500 0000
    mm->stack_start = 0x7FFF FFFF    // 栈区
    mm->stack_end = 0x8FFF FFFF
}

// 堆区和栈区所谓的区域调整,本质就是修改各个区域的end或者start
// 一般就是定义局部变量;malloc new堆空间,这样就会扩大栈区或者堆区
// 函数调用完毕; free申请的空间,这样就会缩小栈区或者堆区

虚拟地址映射到物理内存

image-20230201010544626

注:在Linux中,虚拟地址就是线性地址

多个进程虚拟地址的映射

image-20230201213049947

为什么存在地址空间

原因1:

1.如果所有的进程都可以直接访问物理内存,万一进程非法越界非法操作就会带来极大的安全隐患?

​ 当很多个进程的数据都放在内存中,如果进程可以直接访问物理内存,那么如果有恶意进程,可能会读取其他

进程的数据(像密码),或者做出其他的非法操作。

  • ​ 当进程有了虚拟地址空间之后,那么虚拟地址就可以通过页表的映射找到对应的物理内存,一旦发生了越界,虚拟地址就无法在页表中找到对应的物理地址,那么物理内存就会拒绝这些非法访问。

原因2(上述提出问题的解答)

首先我们再来回顾一个概念:

​ 进程 = 内核数据结构 + 进程对应的代码和数据

  • 我们对于上述问题不理解的地方在于,global_value的地址是相同的,为什么打印的值不同

  • 这是因为子进程或者父进程取得global_value的地址都是各自进程地址空间的虚拟地址,而打印的值其实是

他们的虚拟地址对应的物理内存上地址的数据。

  • 一开始子进程和父进程global_value的值都为100,这个数据存储在物理内存的一个地方,并且这个数据被子进程和父进程共享。这也就是global_value地址相同,被打印出的值也想同。

  • 执行6秒钟之后,子进程的global_value的值需要做出修改,如果直接在子进程对应的物理内存处对global_value的值做出修改,那么就会对父进程造成影响(因为这个数据是父子进程共享的)。

  • 因此当遇到这种问题的时候,操作系统就会进行写时拷贝,将不同进程的数据进行分离

  • 写时拷贝:被多个进程共享的数据,任何一方尝试写入时,操作系统都会先对这个数据进行拷贝,拷贝到物理内存的其他地方,并更改页表映射(这样尝试写入的一方就有了独属自己的数据),然后再让进程进行修改。

  • 这样的话子进程就有了独属自己的数据,修改global_value的值并不会对父进程global_value的值造成影 响,这也就是为什么相同的地址,但是值不同。

**注:**

1.因为进程的独立性,一个进程对被共享的数据做修改,如果影响了其他进程,不能称之为独立性。

2.上述所说的一切都是操作系统来完成的(写时拷贝等) 

所以进程地址空间存在的原因:进程地址空间的存在,可以更方便的进行进程和进程代码数据的解耦,保

证了进程独立性这样的特征。

image-20230201234342459

原因3(重新再一次理解进程地址空间):

1.我们写的可执行程序里面有没有地址?(在还没有加载到内存中时,可执行程序还在磁盘)
    答案是有的,在我们对程序进行汇编时,程序之间的跳转就是要依靠地址来跳转的,再就是链接动态库,也就是将相应的地址,加载到程序中。   // 这个时候的地址就叫做逻辑地址
    // 注:此处不会对堆进行编址,因为堆空间是动态开辟的,是在运行时,申请空间的时候进行编址的

2.编译器在对我们的代码进行编址的时候,是按照虚拟地址空间的方式对我们的代码和数据进行编址(有代码区,数据区等等)
    
3.上面所说道的地址,是我们程序内部的逻辑地址,当我们程序的代码和数据加载到内存之后,代码和数据在内存中储存位置的地址就是它们的物理地址,只要将代码和数据加载到内存,它们就会具备物理地址。
    注:进程地址空间的初始化,就是依靠这些逻辑地址来初始化的
    
4.CPU在运行进程时,会读取物理内存中的指令(而这些指令是具有逻辑地址的,但是在这种场景下,这些地址被叫做虚拟地址),根据指令中的地址来运行代码,也就是根据虚拟地址来运行这些代码。
    // 读取物理内存中指令的地址 -> 加载到CPU -> cpu再通过虚拟地址的映射找到对应的物理地址读取代码和数据 -> 再读取指令的下一条地址 ->      ;就这样一直循环,一直运行。
    
    所以CPU从始至终是没有读取物理地址的。
    
存在进程地址空间的原因31.让进程用统一的视角来看待进程对应的代码和数据等各个区域,方便使用。
   2.让编译器也用统一的视角来进行编译代码。
   3.而进程地址空间和编译器编址的规则是一样的,所以编译器编址完成,虚拟地址直接拿编译器编好逻辑地址来用即可

image-20230202005408389
al_value的地址是相同的,为什么打印的值不同

  • 这是因为子进程或者父进程取得global_value的地址都是各自进程地址空间的虚拟地址,而打印的值其实是

他们的虚拟地址对应的物理内存上地址的数据。

  • 一开始子进程和父进程global_value的值都为100,这个数据存储在物理内存的一个地方,并且这个数据被子进程和父进程共享。这也就是global_value地址相同,被打印出的值也想同。

  • 执行6秒钟之后,子进程的global_value的值需要做出修改,如果直接在子进程对应的物理内存处对global_value的值做出修改,那么就会对父进程造成影响(因为这个数据是父子进程共享的)。

  • 因此当遇到这种问题的时候,操作系统就会进行写时拷贝,将不同进程的数据进行分离

  • 写时拷贝:被多个进程共享的数据,任何一方尝试写入时,操作系统都会先对这个数据进行拷贝,拷贝到物理内存的其他地方,并更改页表映射(这样尝试写入的一方就有了独属自己的数据),然后再让进程进行修改。

  • 这样的话子进程就有了独属自己的数据,修改global_value的值并不会对父进程global_value的值造成影 响,这也就是为什么相同的地址,但是值不同。

**注:**

1.因为进程的独立性,一个进程对被共享的数据做修改,如果影响了其他进程,不能称之为独立性。

2.上述所说的一切都是操作系统来完成的(写时拷贝等) 

所以进程地址空间存在的原因:进程地址空间的存在,可以更方便的进行进程和进程代码数据的解耦,保

证了进程独立性这样的特征。

[外链图片转存中…(img-F5WDYPiG-1743560477298)]

原因3(重新再一次理解进程地址空间):

1.我们写的可执行程序里面有没有地址?(在还没有加载到内存中时,可执行程序还在磁盘)
    答案是有的,在我们对程序进行汇编时,程序之间的跳转就是要依靠地址来跳转的,再就是链接动态库,也就是将相应的地址,加载到程序中。   // 这个时候的地址就叫做逻辑地址
    // 注:此处不会对堆进行编址,因为堆空间是动态开辟的,是在运行时,申请空间的时候进行编址的

2.编译器在对我们的代码进行编址的时候,是按照虚拟地址空间的方式对我们的代码和数据进行编址(有代码区,数据区等等)
    
3.上面所说道的地址,是我们程序内部的逻辑地址,当我们程序的代码和数据加载到内存之后,代码和数据在内存中储存位置的地址就是它们的物理地址,只要将代码和数据加载到内存,它们就会具备物理地址。
    注:进程地址空间的初始化,就是依靠这些逻辑地址来初始化的
    
4.CPU在运行进程时,会读取物理内存中的指令(而这些指令是具有逻辑地址的,但是在这种场景下,这些地址被叫做虚拟地址),根据指令中的地址来运行代码,也就是根据虚拟地址来运行这些代码。
    // 读取物理内存中指令的地址 -> 加载到CPU -> cpu再通过虚拟地址的映射找到对应的物理地址读取代码和数据 -> 再读取指令的下一条地址 ->      ;就这样一直循环,一直运行。
    
    所以CPU从始至终是没有读取物理地址的。
    
存在进程地址空间的原因31.让进程用统一的视角来看待进程对应的代码和数据等各个区域,方便使用。
   2.让编译器也用统一的视角来进行编译代码。
   3.而进程地址空间和编译器编址的规则是一样的,所以编译器编址完成,虚拟地址直接拿编译器编好逻辑地址来用即可

[外链图片转存中…(img-xDjsKmUO-1743560477299)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值