Linux 环境变量 & 进程地址空间(学习 笔记)

系统中 有个 PATH 是用于维护系统可执行程序路径的程序的变量 和 windows下类似

echo $HOME

输出用户home路径

~ | cd ~ 是回家 而~ 就代表的 是 & HOME 的环境d变量

环境变量相关命令

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

使用export 设置完成环境变量后可以使用 unset 命令取消环境变量

env 显示所有的环境变量

unset 清除环境变量

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

如何调用系统函数 查看系统环境变量

环境变量有全局属性可以被子进程继承下去

chen@chen  ~/test_linux/path_test  cat test.c

#include <stdio.h> 
#include <stdlib.h> 
​
​
int main(int *argc,char *argv[]){ 
​
printf("%s\n",getenv("PATH")); 
​
return 0; 
​
} 

chen@chen  ~/test_linux/path_test  ./test /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl

pwd 命令

查看用户当前位置

三种获取环境变量的方法:

  • 命令行的第三个参数

    命令行参数

    • int argc

    • char *argv[]

    • char * env[]

  • 通过 stdlib.h 下的getenv()函数

  • 通过导入 第三方变量实现

    extern char ** environ;

程序地址空间

在 Linux中%p 输出的地址 都是虚拟地址 物理地址用户看不到 由操作系统统一管理

OS 负责将虚拟地址转换成物理地址

不同进程的地址相同输出的数据不同的原因:

不同 进程 相同的变量 地址相同 虚拟地址不同 是根据虚拟地址通过映射表找到不同的物理地址上,故数据不同


因为进程具有独立性,一个进程对被共享的数据作修改,如果影响了其他进程,就不能称为独立性

在两个进程使用同一个物理地址的时候,任何一方尝试写入,OS 先进进行数据拷贝,更改页表映射,然后再让进程进行修改

这叫做写时拷贝,主进程和子进程谁先执行return 谁先执行写时拷贝

地址空间的本质 : 是内核的一种数据结构! mm_struct

[区域起始地址 ,[虚拟地址],区域结束地址]

struct mm_struct
{
    uint32_t code_start,code_end;
    uint32_t data_start,data_end;
    uint32_t heap_start,heap_end;
    uint32_t stack_start,stack_end;
    // 区域调整就是修改 各个区域的 end  && start
}

为什么存在地址空间?

如果进程访问物理内存,进程越界非法操作非常不安全.

可以方便的进行进程和进程之间的解耦保证了进程独立性

让进程用统一的视角来看待进程对应的代码和数据等各个区域,方便使用编译器也用统一的视角来进行编译代码

Linux 进程

什么是进程?

  • 进程是一个正在运行的程序,是加载到内存中的程序实体。

  • 进程与程序相比,具有动态属性。

进程先描述后组织,即进程是先通过进程控制块(PCB)描述,再由操作系统组织和管理的。

PCB 概念 (进程控制块)

PCB(进程控制块)是操作系统中用来描述进程的基本结构,用来存储进程的所有属性。

struct task_struct {
    // 该进程的所有属性
    // 该进程对应的代码地址和属性地址
};

task_struct 是内核结构体,表示一个内核对象,它将代码和数据与具体的进程关联起来。内核通过 task_struct 管理进程,即通过 PCB 将进程信息与内存中的代码关联,实现进程的组织和调度。

Linux 查看进程的方式

常见的系统进程查看方式

在 Linux 中,命令行上的进程的父进程通常是 bash。可以通过系统调用函数来查看进程:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
​
int main() {
    while(1) {
        printf("%d\n", getpid());
    }
    return 0;
}

常见的用于查看父进程的系统调用是 getppid()

printf("%d\n", getppid());

另外,通过查看 /proc 文件目录也可以查看当前进程的详细信息。

image-20240910151424004

fork 初识

fork() 是创建子进程的系统调用,返回两个值:父进程返回子进程的 PID,子进程返回 0。父子进程共享代码,但数据独立,各自拥有自己的空间。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
​
int main() {
    fork();
    printf("%d -- | -- %d\n", getpid(), getppid());
    return 0;
}

运行效果如下:

chen@chen:~$ ./test
13977 -- | -- 13667
13978 -- | -- 13977

其中 13667 是 bash 进程。

进程状态

进程的状态包括:运行、新建、就绪、挂起、阻塞、等待、停止、死亡。

进程的状态会影响操作系统的调度和管理。一个 CPU 只有一个运行队列,所有进程都需要排队等待 CPU 资源。

static const char * const task_state_array[] = {
    "R (running)",       // 运行状态
    "S (sleeping)",      // 浅度睡眠
    "D (disk sleep)",    // 深度睡眠
    "T (stopped)",       // 停止
    "t (tracing stop)",  // 跟踪状态
    "X (dead)",          // 死亡
    "Z (zombie)"         // 僵尸状态
};
  • 运行状态:被调度并处于运行队列的进程。

  • 阻塞状态:等待外设响应的进程。

  • 挂起状态:进程数据与代码被交换到外部设备,暂时被挂起。

深度睡眠的进程无法被操作系统直接杀掉,除非断电或等待系统自动恢复。

使用命令 ps axj | head -1 && ps -axj | grep bash 可以查看进程状态。

chen@chen:~$ ps axj | head -1 && ps -axj | grep bash
PPID   PID   PGID   SID TTY     TPGID STAT  UID  TIME COMMAND 
15572  15707  15706  15572 pts/0    15706 S+    1000  0:00 bash

S+ 中的 + 表示前台进程,若无 + 则表示后台进程。

僵尸状态 (Z):子进程退出后,父进程未读取其状态,子进程进入僵尸状态。

孤儿进程:父进程退出后,孤儿进程被 init 进程(PID 1)收养。

进程的优先级

什么是优先级?

进程优先级用于决定任务的调度顺序,某些任务需要优先执行。

Linux 的优先级机制

在 Linux 中,进程的优先级由两个数值决定:PRI 和 NI。PRI 是基本优先级,NI 是 Nice 值。

优先级 = 老的优先级 + NICE

Nice 值的取值范围是 [-20, 19],Nice 值越低,优先级越高。可以使用 top 命令,按 R 键修改优先级。

进程的特性

  • 竞争性:根据优先级高效完成任务。

  • 独立性:多个进程独享资源,互不干扰。

  • 并行:多个进程在多个 CPU 上同时运行。

  • 并发:在一个 CPU 上通过进程切换,在同一段时间内推动多个进程。

进程切换

进程切换时需要保存和恢复进程的上下文。操作系统会在不同进程之间进行切换,保证每个进程的上下文信息正确保存和恢复。

进程控制

进程创建

fork进程

fork返回值

  • 子进程返回0

  • 父进程返回1

在C语言或C++中 return 0 叫做程序结束

在操作系统层面叫做进程推出的时候对应的退出码标定程序进程退出的时候是否正确

./XXXX 运行一个进程

echo $? 输出最近一次进程的返回值( 退出码)

一般退出码 0代表成功 非0 代表失败

退出码都有自己的退出码相应的文字描述供开发者调试 也可以自定义 或者使用String.h中的 strerror(int error) 函数查看系统内部定义的错误码

进程退出的情况

  • 代码跑完 结果正确 -- return 0;

  • 代码跑完 结果不正确 return !0;

  • 代码没跑完,程序异常 退出码无意义

进程退出

  • main 函数return返回

  • 任意地方调用Exit(int) <unistd.h> 函数 //库函数 终止进程的时候会主动刷新缓冲区

  • _exit 函数 /// 系统函数 不会刷新缓冲区

进程等待

  • 进程为什么会等待?

    回收子进程,获取子进程信息

    父进程等待的时候会回收子进程的信息会避免僵尸进程

进程等待的方法

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

pid_t wait(int*status)
    //成功返回被等待的进程pid 失败返回-1
    //参数输出型 获取子进程的退出状态可为null
pid_t waitpid(pid_t pid, int*status,int options);
    //正常返回的时候返回waitpid返回收集到子进程的进程ID
	//如果调用出错则errno 被设置到相应的错误所在

	//参数Pid = -1等待任意一个子进程=wait
	//pid>0 等待进程ID 和 pid 相等的子进程
	//status:
	// 为NULL则不关心子进程的运行状态
	// 根据参数将子进程的退出信息返回给父进程
	

	// options -
	//linux宏
	//WIFEXITED(status):正常终止为真
	//WEXITSTATUS(status)
	//WIFEXITED为非0提取退出码
	//WNOHANG: 非阻塞
	//子进程没有退出父进程结束的时候立即返回

status返回信息通过二进制位的方式查看

分为0-7和8-15位的方式

其中8-15位为正常终止退出状态

若被信号杀掉 8-15位为未使用

阻塞等待和非阻塞等待

  • 阻塞等待:子进程运行的时候父进程等待

  • 非阻塞等待:子进程运行的时候父进程执行其他同时询问子进程是否执行完毕

进程替换

让子进程执行父进程代码的一部分

让子进程执行一个全新的程序加载磁盘上的制定程序

替换函数

将程序加载到内存中让制定进程程序执行

#include<unistd.h>

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,const char* argv[]);
int execve(const char * path,char * const argv[],char * const envp[]);

调用成功 直接加载新程序

调用失败 直接返回 -1

所以exec 没有成功的返回值

  • l:表示参数使用列表 list

  • v:表示参数使用数组 vector

  • p:自动搜索环境变量PATH

  • e:自己维护环境变量env

其中execve是系统调用 其余是对execve的封装

手搓myshell

test:test.c
        gcc -o $@ $^ -std=c99 -DDEBUG 
.PHONY:clean
        rm -rf test

test.cpp

#include <stdio.h>      // 标准输入输出库
#include <stdlib.h>     // 标准库,提供内存分配、进程控制等功能
#include <string.h>     // 字符串操作库
#include <unistd.h>     // 提供系统调用接口,如 fork、exec 系列函数
#include <assert.h>     // 提供断言功能,用于调试
#include <sys/types.h>  // 定义数据类型,如 pid_t
#include <sys/wait.h>   // 提供进程等待的相关函数,如 waitpid

#define MIX_CMD 1024    // 命令输入的最大长度
#define OPT 64          // 命令参数(选项)的最大数量

char command[MIX_CMD];  // 用于存储用户输入的命令
char * myarev[OPT];     // 用于存储分割后的命令参数

// shell 函数:负责获取用户输入并执行相应命令
void shell() {
    // 清空命令缓冲区
    memset(command, 0, sizeof(command));

    // 提示用户输入命令
    printf("想要干什么@你可以告诉我#$: ");
    fflush(stdout);  // 刷新输出缓冲区,确保提示符显示

    // 读取用户输入的命令,保存在 command 数组中
    char *s = fgets(command, sizeof(command) - 1, stdin);
    (void)s; 
    //这一行的作用是告诉编译器你故意不使用 s 变量,防止出现编译器警告。
    assert(s != NULL);  // 断言检查输入是否有效

    // 移除末尾的换行符
    command[strlen(command) - 1] = 0;

    int i = 1;
    // 使用 strtok 分割用户输入的命令,第一个参数存储在 myarev[0]
    myarev[0] = strtok(command, " ");

    // 继续分割命令的参数,存储在 myarev 数组中
    while (myarev[i++] = strtok(NULL, " "));

#ifdef DEBUG
    // 如果定义了 DEBUG 宏,可以打印命令参数用于调试
    // for(int i = 0; myarev[i]; i++) {
    //     printf("%s \n",myarev[i]);
    // }
#endif

    // 创建子进程
    pid_t id = fork();

    // 在子进程中执行用户输入的命令
    if (id == 0) {
        printf("喵喵喵\n");  // 子进程提示信息(可以去掉或修改)
        execvp(myarev[0], myarev);  // 执行命令
        exit(0);  // 如果 execvp 失败,子进程退出
    }

    // 父进程等待子进程执行完成
    waitpid(id, NULL, 0);
}

int main() {
    // 进入主循环,不断获取用户输入并执行命令
    while (1) {
        shell();
    }
}