Linux进程概念(下)

简介: 本文详细的介绍了环境变量和进程空间的概念及其相关的知识。

1. 命令行参数

main函数也可以带参数的,如下

#include <stdio.h>
int main(int argc, char* argv[])
{
   
   
    int i = 0;
    for (i = 0; i < argc; ++i)
    {
   
   
        printf("%d:%s\n", i, argv[i]);
    }
    return 0;
}

image-20240426171713697

命令行整个一行是一个大的字符串,以空格作为分隔符,被分割成了5个子串。

  • 第一个参数argc是,命令行以空格作为分隔符有几个字符串,比如上面是5个字符串,argc就是5。
  • 第二个参数argv是一个指针数组,保存着每个子串的地址。并且有效元素要比实际上命令行的子串多一个,最后一个一般以NULL结尾。

识别这些字符串子串和传参是操作系统自动帮我做的。为什么main函数要这么设计呢?

比如我们想实现一个命令行版的计算器:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
   
   
    if (argc != 4)
    {
   
   
        printf("Use it incorrectly, please conform to the following usage.\nUsage:%s op[-add|sub|mul|div] d1 d2", argv[0]);
    }
    int x = atoi(argv[2]);
    int y = atoi(argv[3]);
    // 一定有四个命令行参数
    if (strcmp(argv[1], "-add") == 0)
    {
   
   
        printf("%d+%d=%d\n",x ,y ,x+y);
    }
    else if (strcmp(argv[1], "-sub") == 0)
    {
   
   
        printf("%d-%d=%d\n",x ,y ,x-y);
    }
    else if (strcmp(argv[1], "-mul") == 0)
    {
   
   
        printf("%d*%d=%d\n",x ,y ,x*y);
    }
    else if (strcmp(argv[1], "-div") == 0)
    {
   
   
        if (0 == y)
        {
   
   
            printf("%d/%d=error!\nZero cannot be used as the divisor.\n",x ,y);
        }
        else
        {
   
   
            printf("%d/%d=%d\n",x ,y ,x/y);
        }
    }
    else
    {
   
   
        printf("Use it incorrectly, please conform to the following usage.\nUsage:%s op[-add|sub|mul|div] d1 d2", argv[0]);
    }
    return 0;
}

使用一下:

image-20240426190236092

由此我们可以理解原来使用的命令和main函数这么设计的原因就是:

比如我们原来用的ls命令(这些命令就是用C语言写的!),带着命令行选项(和我们上述写的命令行计算器带选项相似), 就可以实现同一选项实现不同功能。

命令行参数(选项),可以支持各种指令级别的命令行选项设置!

2. 环境变量

2.1 环境变量的概念

基本概念:

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。

命令env查看当前操作系统所有环境变量。

image-20240427152643373

系统重会存在大量的环境变量,每一个环境变量都有它自己的特殊用途用来完成特定的系统功能~!

常见环境变量:

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL : 当前Shell,它的值通常是/bin/bash。
  • PWD:表示当前工作目录的路径。

查看环境变量方法:

echo $NAME NAME:你的环境变量名称

2.2 环境变量的使用和一些问题

为什么执行系统命令的时候不需要带./,但是执行我们自己的可执行程序却需要呢?

首先使用命令echo $PATH查看环境变量
image-20240427103727659
不用带./的原因就是,ls等这些系统级别的指令都存储在这个环境变量里,执行的时候系统会依次检索这些目录。

如果想让我们的可执行程序不带./就可以执行,很简单,把可执行程序所在的目录添加到环境变量即可。

使用命令PATH=$PATH:你要添加的目标目录(该命令是直接接到原有的环境变量之后)
image-20240427105055255

此时再次来查看环境变量的路径
image-20240427105156622
发现我们的可执行程序所在的目录已经在环境变量里了,来用一下~
image-20240427105321119
此时,就不用带./了捏!

如何删除呢?

直接用命令PATH=原目录(意思就是直接复制一份老的环境变量直接覆盖即可)
image-20240427105829339
这时不带./我们的可执行程序就又跑不了了。

如果我们干个“坏事”,把环境变量直接整没,PATH=""
image-20240427110425373
然后,然后就会这样了,几乎所有命令都不能使用了,怎么办捏,系统是不是就崩了!其实不用过于担心, 重新登陆一下系统就好了(果然重启解决99%的问题)。

我们对环境变量的修改,仅仅只在内存级别的修改,我们知道内存是易失性存储器,只要系统重启就会恢复原有的模样。默认更改环境变量,只限于本次登录,如果重新登录的话环境变量会自动恢复。

如果我们直接把我们自己的可执行程序直接拷贝到默认的环境列表中,也可以做到如此效果,这个操作我们称之为程序安装

我们在登陆Linux的时候发现
为什么普通用户默认所处目录/home/XXX而超级用户所处/root呢?
登陆的时候:

  1. 输入用户名和密码
  2. 认证
  3. 形成环境变量(PATH、PWD、HOME等)
  4. 根据用户名初始化HOME=/rootHOME=/home/XXX
  5. cd $HOME

2.3 获取环境变量

  1. 学习一个调用,获取环境变量:getenv(const char *name)

image-20240427153455700

那么环境变量的作用体现在哪里呢?可以实现系统级别的过滤,比如下段简单的代码:

#include <stdio.h>
#include <stdlib.h>
int main()
{
   
   
    char *user = getenv("USER");
       // 如果用户不是超级用户,直接返回
    if (strcmp(user, "root"))
    {
   
   
        printf("The user is incorrect,please switch users!\n");
        return 1;
    }
    printf("%s is my command test\n", user);
    return 0;
}

image-20240427155625887

当前不是超级用户,执行非法。我们切换一下用户再次执行。

image-20240427160315422

成功执行!

  1. 在学习命令行参数的时候我们知道,main函数可以带两个参数,那么只能带两个参数吗?main函数其实还可以带第三个参数的:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[], char* env[])
{
   
   
    int i = 0;
    for (i = 0; env[i]; ++i)// 这个和argv相似,最后都会放一个NULL
    {
   
   
        printf("pid:%d,env[%d]:%s\n", getpid(), i, env[i]);
    }
    return 0;
}

image-20240427170822473

可以看到,与我们直接在命令行使用env命令基本相似。

系统启动我们的程序的时候,可以选择给我们的进程(main)提供两张表:

  1. 命令行参数表
  2. 环境变量表
  1. 如果不想使用命令行参数来查看环境变量表,可以使用C语言为我们提供的一个全局标量environ

image-20240427195554683

int main()
{
   
   
    extern char **environ;
    int i = 0;
    for ( ; environ[i]; ++i)
    {
   
   
        printf("%d: %s\n", i, environ[i]);
    }
    return 0;
}

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。

执行一下,同样会得到我们想要的效果。

2.4 深入理解环境变量

上面我们做过实验,用PATH=""将路径直接覆盖为空,会导致大部分命令使用不了,但是重新登陆之后又会恢复如初。当前我们直接更改的是bash进程内部的环境变量信息!每一次重新登陆,都会给我们形成新的bash解释器并且新的bash解释器自动从某一位置读取形成自己的环境变量表信息。

命令行启动的进程都是shell/bash的子进程,子进程的命令行参数和环境变量,是父进程bash给我们传递的!那么父进程的环境变量信息又从哪里来呢?

环境变量信息是以脚本配置文件的形式存在的。

进入家目录下,查看隐藏文件,会发现有一个.bash_profile的文件

image-20240427175808519

当我们登录时,bash会自动加载该文件,配置文件中的内容,为我们bash进程形成一张环境变量信息表!

我们可以通过一些特殊的指令,添加我们自己的环境变量:

把这种定义的变量称之为shell定义的本地变量,但是它并没有存在bash的环境变量表中。使用命令export 你的环境变量名,把你的环境变量导出到它自己的环境表进程中,再次用env查看发现,bash和我们的可执行程序(也就是子进程)都有了我们定义的环境变量。

image-20240427192354987

也可以直接在定义环境变量的同时直接导出

export 你的环境变量名=内容

image-20240427192741892

但是该自定义环境变量依旧是存储在内存里的,一旦重新登陆,又消失不见了,那么应该怎么才能永久保存呢?

再次进入bash_profile文件,加入我们对应的自定义环境变量:
image-20240427193858073

保存退出,再次重新登陆,环境变量便永久存在了
image-20240427194212405

命令行是支持定义本地变量的,比如:
image-20240427205112232

本地变量 vs 环境变量

  • 本地变量只在bash进程内部有效,并且不会被子进程继承。
  • 环境变量通过让所有子进程继承的方式,实现自身的全局性。

当我们清空环境变量时会导致大部分命令使用不了,在Linux中这大部分是用不了的命令,是在磁盘中真正存在并且需要由fork创建子进程来执行的命令。但是在shell中还有一种命令,并不会创建子进程,它的执行风险非常低,由bash自己来执行,就等同与bash内一个函数,诸如echoexport这样的命令依旧能够继续使用。

Linux的命令分类:

  1. 常规命令。需要shell、fork创建子进程,让子进程执行的命令。
  2. 内建命令。shell命令行的一个函数,echo就是内建命令,所以可以直接读取shell内部定义的本地变量。

2.5 环境变量相关的命令

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量

3. 进程地址空间

3.1 基本概念

在学习C/C++时,我们学习过这样的空间布局:

image-20240429170909420

用一段代码验证下:

#include <stdio.h>
#include <stdlib.h>

int un_gval;
int init_gval = 100;

int main(int argc, char* argv[], char* env[])
{
   
   
    printf("code address:%p\n", main);                   
    const char *str = "hello Linux";                     
    printf("read only char address:%p\n", str);                             
    printf("init global value address:%p\n", &init_gval);

    char *heap1 = (char*)malloc(100);
    char *heap2 = (char*)malloc(100);
    char *heap3 = (char*)malloc(100);
    char *heap4 = (char*)malloc(100);
    printf("heap1 address:%p\n", heap1);
    printf("heap2 address:%p\n", heap2);
    printf("heap3 address:%p\n", heap3);
    printf("heap4 address:%p\n", heap4);

    printf("stack address:%p\n", &str);
    printf("stack address:%p\n", &heap1);
    printf("stack address:%p\n", &heap2);
    printf("stack address:%p\n", &heap3);
    printf("stack address:%p\n", &heap4);

    int i = 0;
    for (; argv[i]; ++i)
    {
   
   
        printf("argv[%d]:%p\n", i, argv[i]);
    }

    for (i = 0; env[i]; ++i)
    {
   
   
        printf("env[%d]:%p\n", i, env[i]);
    }
    return 0;
}

运行结果:
image-20240429160704161

堆向上生长,栈向下生长,堆栈相对而生也得已验证。栈区虽然整体是向下生长但是局部是向上使用的。

当我们定义一个int整型取地址时,我们发现所得到地址只有一个字节,但是int却是四个字节,难道不应该得到四个字节的地址吗。

其实我们取地址一般都取到的是这个变量的地址,所以能得出一个结论:C/C++进程访问的本质是起始地址+偏移量的访问形式!

再来段代码感受一下:

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

int g_val = 100;
int main()
{
   
   
    pid_t id = fork();
    if (id == 0)
    {
   
   
        int count = 5;
        while (1)
        {
   
   
            printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
            sleep(1);
            if (count == 0)
            {
   
   
                g_val = 200;
                printf("sub-process is changed: 100->200\n");   
            }
            count--;
        }
    }
    else
    {
   
   
        // father
        while (1)
        {
   
   
            printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
            sleep(1);
        }
    }
}

输出结果:
image-20240429174555649

g_val的值未改动之前:

我们发现,输出出来的变量值和地址是一模一样的,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。

g_val的值改动之后,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量!
  • 但地址值是一样的,说明,该地址绝对不是物理地址!
  • 在Linux地址下,这种地址叫做虚拟地址!
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理!

OS必须负责将虚拟地址转化成物理地址。

Untitled (1)

上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

这里的写时拷贝是在物理内存中的,由操作系统来做,并且不影响上层语言。

地址空间也要被OS管理起来,每一个进程都要有地址空间,在系统中,一定要对地址空间做管理!

如何管理地址空间呢?(经典六字)

先描述,再组织。

地址空间最终一定是一个内核的数据结构对象!就是一个内核的结构体!

在Linux中,进程/虚拟地址空间这个东西就是一个结构体,大概描述一下:

struct mm_struct
{
   
   
    long code_start;
    long code_end;
    long data_start;
    long data_end;
    long heap_start;
    long stack_start;
    long stack_end;
}

3.2 为什么要有地址空间

  1. 让进程以统一的视角看待内存

    所以任意一个进程,可以通过地址空间+页表的方式将乱序的数据内存,变成有序,并且分门别类的规划好!无论什么改动,我们只需改变映射关系即可,不用在物理内存中挨个去找,大大提高了管理内存的效率。

  2. 其实页表除了保存虚拟地址和物理地址外,还有一个访问权限字段
    image-20240502110940484

所以说,存在虚拟地址空间可以有效的进行进程访问内存的安全检查!

  1. 将进程管理和内存管理进行解耦

通过页表,让进程映射到不同的物理内存处,从而实现进程的独立性

挂起在Linux中如何体现呢?

image-20240502160546239

在页表中除了访问权限字段之外,还有检测对应的物理地址是否在内存当中,如果查询页表时标记字段标记为0(假设0表示该地址不在内存中),那么就认为该进程为挂起状态。

目录
相关文章
|
14天前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
3月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
132 1
|
26天前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
75 34
|
9天前
|
Linux
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
27 7
|
8天前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
30 5
|
8天前
|
Linux 应用服务中间件 nginx
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
19 3
|
8天前
|
存储 算法 数据处理
进程基础:概念、状态与生命周期
进程是操作系统进行资源分配和调度的基本单位,由程序段、数据段和进程控制块(PCB)组成。线程是进程中更小的执行单元,能独立运行且共享进程资源,具有轻量级和并发性特点。进程状态包括就绪、运行和阻塞,其生命周期分为创建、就绪、运行、阻塞和终止阶段。
46 2
|
1月前
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
65 16
|
2月前
|
消息中间件 Linux
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
177 20
|
4月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
569 58