【Linux】-- 进程概念
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
一、进程概念
课本概念进程是程序的一个执行实例是正在执行的程序。
内核观点进程是承担系统资源CPU时间、内存的实体。
当我们写完代码之后编译连接就形成一个可执行程序.exe本质是二进制文件在磁盘上存放着。双击这个.exe文件把程序运行起来就是把程序从磁盘加载到内存然后CPU才能执行其代码语句。当把程序加载到内存后这个程序就叫做进程。所有启动程序的过程本质都是在系统上创建进程双击.exe文件也不例外
二、PCB
1.什么是PCB
根据操作系统管理是先描述再组织那么操作系统是如何描述进程的呢先预想一下肯定是先描述进程信息然后再把这些信息用数据结构组织起来进行管理。那么进程都有哪些信息呢使用
ps axj
命令查看系统当中的进程也就是正在运行的程序
看到进程的属性至少有PPID、PID、PGID、SID、TTY、TPGID、STAT、UID、TIME、COMMAND。
进程信息被放在一个叫做进程控制块PCB(Process Control Block)的数据结构中它是进程属性的集合。
操作系统创建进程时除了把磁盘上的代码和数据加载到内存以外还要在系统内部为进程创建一个task_struct是一个struct。
2.什么是task_struct
Linux操作系统的下的PCB就是task_struct所以task_struct是PCB的一种在其他操作系统中的PCB就不一定叫task_struct。
创建进程不仅仅把代码和数据加载到内存还要为进程创建task_struct所以进程不仅仅是运行起来的程序更准确的来说进程是程序文件内容和操作系统自动创建的与进程相关的数据结构其实进程还包括其他内容今天先说这两个。
操作系统对每一个进程进行了描述这就有了一个一个的PCBLinux中的PCB就是task_struct这个struct会有next、prev指针可以用双向链表把进程链接起来task_struct结构体的部分指针也可以指向进程的代码和数据
所有运行在系统里的进程都以task_struct作为链表节点的形式存储在内核里这样就把对进程的管理变成了对链表的增删改查操作。
增当生成一个可执行程序时将.exe文件存放到磁盘上双击运行这个.exe程序时操作系统会将该进程的代码和数据加载到内存并创建一个进程对进程描述以后形成task_struct并把插入到双向链表中。
删进程退出就是将该进程的task_struct节点从双向链表中删除操作系统把内存中该进程的代码和数据进行释放。
3.task_struct包含内容
标示符: 描述本进程的唯一标示符用来区别其他进程。
状态: 任务状态退出代码退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。
还有一些其他信息。下面解释task_struct包含内容的具体含义。
三、task_struct内容详解
1.查看进程
1通过系统目录查看
proc是一个系统文件夹在根目录下通过ls可以看到该文件夹
可以通过
ls /proc
命令查看进程的信息数字是PID
如果想查看进程信息比如查看PID为989的进程信息使用命令
ls /proc/PID
查看
2通过ps命令查看
使用
ps aux
命令查看进程可以看到所有进程
如果结合grep可以查看某一个进程
比如想查看包含proc的进程可以使用如下命令
ps aux | head -1 && ps aux | grep proc | grep -v grep
3通过top命令查看
也可以通过
top
命令查看
4通过系统调用获取进程PID和父进程PPID
-
获取进程ID函数getpid和getppid
获取进程ID和获取父进程ID可以通过以下方式进行获取其中pid_t是short类型变量
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);//获取当前进程ID
pid_t getppid(void);//获取当前进程的父进程ID
-
获取当前进程ID
获取当前进程process.c
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("hello linux!:pid:%d\n",getpid());//获取当前进程ID
sleep(1);
}
return 0;
}
Makefile
process:process.c
gcc -o $@ $?
.PHONY:clean
clean:
rm -f process
运行之后就获取到了当前进程的PID即进程号
关闭进程可以通过ctrl+c或者来关闭进程。另开一个窗口现在通过ps来查看进程
这也就验证了getpid获取到的是PID。
-
获取父进程ID
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("hello linux!:pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
使用ps命令查看发现父进程的ID是11081但是11081同时也是bash的子进程
这是因为运行命令行的命令有风险命令行出错了不能影响命令行解释因此在命令行上运行的命令基本上父进程都是bash。
使用如下命令可查看到进程内部的所有属性信息
ls /proc/当前进程ID -al
当进程退出时就没有/proc/18448这个文件夹了ctrl c后再去查看文件夹已经不存在了
2.状态
之前写代码的返回值是0 这个0是进程退出时的退出码这个退出码是要被父进程拿到的返回给系统父进程通过系统拿到。比如以下代码的退出码是0
#include<stdio.h>
int main()
{
printf("hello linux!\n");
return 0;
}
那么使用
echo $?
就可以查看到进程退出码为0
假如将退出码改为99
那么程序运行后的退出码也变成了99
所以状态的作用是输出最近执行的命令的退出码。
3.优先级
权限指的是能不能而优先级指的是已经能了有权限了但是至于什么时候执行得先排队 这就像在餐馆点餐结帐出小票之后已经可以拿到餐食了但是什么时候能拿到呢需要排队在这个过程中是否出小票就代表是否有权限排队取餐就代表的是优先级。
4.程序计数器
当CPU执行程序时执行当前行指令时怎么知道下一行指令是什么呢?程序计数器pc中存放下一条指令的地址当操作系统执行完当前行指令后pc自动会++直接执行下一行命令。
内存指针可以通过task_struct中的内存指针通过PCB 找到进程的代码和数据。
5.上下文数据
当操作系统维护进程队列时由于进程代码可能不会在很短时间就能执行完毕假如操作系统也不会在执行一个进程时让其他进程一直等待直到当前进程执行完毕那可能当前进程需要执行很久才执行完毕其他进程会一直处于等待状态这不合理。那么操作系统在实际执行进程调度时按时间片分配执行时间时间片一到就切换下一个进程。时间片是一个进程单次运行的最长时间。
比如有4个进程在40ms之内先让第一个进程运行10ms时间一到就算没有运行完毕就把第一个进程从队列头移动到队列尾再让第二个进程运行10ms。40ms后使得用户感知到这4个进程都推进了其实本质上是通过CPU的快速切换完成的。
有可能在一个进程的生命周期内被调度成百上千次。比如CPU有5个寄存器进程A正在运行时时间片到了被切走的时候会把CPU里和进程A相关的保存到寄存器里面的临时数据带走。当进程B调度完后再次调度进程A的时候会把进程A里面保存的临时数据再恢复到CPU的寄存器当中继续上次切走时的状态继续运行因此保护上下文能够保证多个进程切换时共享CPU。
6.I/O状态信息
文件操作有fopen、fclose、fread、fwrite等函数其实是进程在操作文件因为在把代码写完之后程序运行起来时操作系统会找到这个进程进程打开文件进行IO操作其实IO都是进程在进行IO所以操作系统需要维护进程和IO信息。
7.记账信息
记录历史上一个进程所享受过的软硬件资源的结合。
四、通过系统调用创建进程
1.使用fork创建子进程
fork用来创建子进程
#include <unistd.h>
pid_t fork(void);//通过复制调用进程创建一个新进程。新进程称为子进程。调用进程称为父进程。
先看一个奇奇怪怪的代码
forkProcess_getpid.c
#include<unistd.h>
#include<stdio.h>
int main()
{
int ret = fork();
if(ret > 0)
{
printf("I am here\n");
}
else
{
printf("I am here,too\n");
}
sleep(1);
return 10;
}
按道理来说要么打印I am here要么打印I am here,too。但是请看执行结果发现两句话都打印了也就是既执行了if又执行了else
再看代码
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret = fork();
while(1)
{
printf("I am here,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
return 10;
}
发现有两个pid和ppid
这说明执行while死循环不只一个执行流在执行 而是两个执行流在执行每一行两个id都是父子关系。这是因为fork之后有两个执行流同时执行while循环。
可以看到bash 16202创建了子进程 16705子进程又创建了子进程 16706
2.理解fork创建子进程
再来说为什么if和else都执行了。
./可执行程序、命令行、fork站在操作系统角度创建进程的方式没有差别都是系统中多了个进程。fork创建出来的子进程和父进程不一样父进程在磁盘上是有可执行程序的运行可执行程序时会把对应的代码和数据加载到内存中去运行。
但是子进程只是被创建出来的没有进程的代码和数据默认情况下子进程会继承父进程的代码和数据子进程的数据结构task_struct也会以父进程的task_struct为模板来初始化子进程的task_struct。因此子进程会执行父进程fork之后的代码来访问父进程的数据。
总结当fork创建子进程时系统里面多了个进程实际上是多了个以父进程为模板的描述进程的数据结构task_struct和以父进程为模板的代码和数据。因此fork之后if和else中的代码都执行了。如果把task_struct比作基因把代码和数据比作事业那么子进程既继承了父进程的基因又继承了父进程的事业。
3.fork后的数据修改
代码是不可以被修改的。 那么数据呢子进程和父进程共享数据当父进程修改数据时子进程看到的数据也被修改了那么父进程就会影响子进程。那这两个进程还具有独立性吗
当父子进程都只读不写数据时数据是共享的。但是这两个进程中的任何一个进程要修改数据都会对对方造成影响这时候作为进程管理者同时也是内存管理者的操作系统就要站出来干涉了。修改时操作系统会在内存中重新开辟一块空间把这部分数据拷贝过去之后再做修改而不是在原数据上做修改这叫做写时拷贝。
写时拷贝是为了维护进程独立性为了防止多个进程运行时互相干扰。而在创建子进程时不会让子进程把父进程的所有数据全部都拷贝一份因为并不是所有情况下都可能产生数据写入所以这就避免了fork时的效率降低和浪费更多空间的问题。因此只有写入数据时再开辟空间才是合理的。
4.fork的返回值
1fork返回值含义
fork出子进程后一般会让子进程和父进程去干不同的事情这时候如何区分父子进程呢fork函数的返回值如下
打印一下fork的返回值
forkProcess_getpid.c
#include<stdio.h>
#include<unistd.h>
iint main()
{
pid_t ret = fork();
while(1)
{
printf("Hello forkProcess,pid = %d,ppid = %d,ret = %d\n",getpid(),getppid(),ret);
sleep(1);
}
return 10;
}
打印结果如下
这说明
- fork准备return的时候子进程被创建出来了。
- 这里有两个返回值由于函数的返回值是通过寄存器写入的 函数返回时把变量值写入到保存数据的空间。所以当父子执行流执行完毕以后有两次返回就有两个不同的返回值就要进行写入谁先返回谁就先写入即发生写时拷贝。
- 给父进程返回子进程的pid的原因是一个父进程可能有多个子进程子进程必须得用pid来进行标识区分所以一般给父进程返回子进程的pid来控制子进程。子进程想知道父进程pid可以通过get_ppid( )来获取。这样就可以维护父子进程了。
2根据fork返回值让父子进程执行不同的功能
通过返回值来让父子进程分流去执行不同的功能
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t ret = fork();
//通过if else来分流
if(ret == 0)//child
{
while(1)
{
printf("I am child, pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
}
else if(ret > 0)//parent
{
while(1)
{
printf("I am parent, pid = %d,ppid = %d\n",getpid(),getppid());
sleep(3);
}
}
else
{
}
return 0;
}
这就让父子进程执行了不同的功能上述代码父进程每隔3秒打印一次子进程每隔1秒打印一次
可以查看到父进程和子进程
通过fork创建出进程再通过if else分离从而让父和子各自执行不同的代码段实现不同的功能。至于父子进程谁先运行是由调度器决定的。
五、进程状态
1.进程状态定义
一个进程从创建而产生至撤销而消亡的整个生命期间有时占有处理器执行有时虽可运行但分不到处理器、有时虽有空闲处理器但因等待某个事件的发生而无法执行这说明进程和程序不相同它是活动的且有状态变化的能够体现一个进程的生命状态可以用一组状态来描述
内核源代码里面的状态定义
/*
* 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 */
};
通过不同状态来对进程进行区分从而对进程进行分类。Linux进程的状态信息保存在进程的task_struct中。
2.进程状态分类
使用如下两条命令都可以查看进程当前状态
ps aux
ps axj
查看到的进程状态
1R-运行状态
R(Running)要么在运行中要么在运行队列里所以R状态并不意味着进程一定在运行中因此系统中可能同时存在多个R状态进程。
如下代码statusType.c:
#include<stdio.h>
int main()
{
while(1);
return 0;
}
运行起来之后一直处于运行状态会发现是R+ 状态其中+表示在前台运行
如果运行时在后面加&就会在后台运行就变成R状态了
后台运行的进程只能用kill -9 进程号来杀掉了
在运行状态的进程是可以被CPU调度的当操作系统切换进程时就会直接在运行队列里选取R状态进程。
2S-浅睡眠状态
S(Sleeping) 进程正在等待某事件完成可以被唤醒也可被杀死浅睡眠状态也叫做可中断睡眠。
比如如下代码
status.c
int main()
{
printf("hello linux\n");
sleep(20);
return 0;
}
在运行后20s内查看status进程的状态发现为S+执行kill命令后该进程被杀死
3D-深睡眠状态
D(Disk sleep)进程正在等待IO不能被杀死必须自动唤醒才能恢复也叫不可中断睡眠状态。
进程等待IO时比如对磁盘写入正在写入时进程处于深度睡眠状态需要等待磁盘将是否写入成功的信息返回给进程因此此时进程不会被杀掉
4T-停止状态
T(Stopped)可以通过发送 SIGSTOP 信号给进程来停止T进程。这个被暂停的进程可以通过发送SIGCONT 信号让进程继续运行。
运行起来的status进程通过SIGSTOP信号被暂停了状态由S+变为T
又通过SIGCONT信号恢复了状态由T变为S
kill -l命令可列出操作系统中所有信号其中18就是SIGCONT信号19就是SIGSTOP信号
因此上述kill SIGCONT 进程号 也可以用kill -18 进程号来代替kill SIGSTOP 进程号 也可以写成kill -19 进程号来代替。
5Z-僵尸状态
当进程退出时所占用的资源不是立即被释放而是要暂时保存进程的所有退出信息来辨别进程死亡的原因(比如代码有问题、被操作系统杀死等)这些数据都保存在task_struct中供父进程或系统读取这就是僵尸状态存在的原因。
当进程退出并且父进程没有读取到子进程退出的返回码时就会产生僵尸进程。僵尸进程会以终止状态保持在进程表中并且会一直等待父进程读取退出状态码。
如下代码statusZombie.cc
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
cout << "child is running" << endl;
sleep(20);
}
}
else
{
cout << "father" << endl;
sleep(50);
}
return 0;
}
Makefile:
statusZombie:statusZombie.cc
g++ -o $@ $^
.PHONY:clean
clean:
rm -f statusZombie
使用如下监控进程脚本
while :; do ps axj | head -1 && ps ajx | grep 进程名 | grep -v grep;sleep 1; echo "####################"; done
来监控进程状态进程运行之后父进程和子进程的状态变成了S
杀掉子进程后子进程的状态变成了Z状态
所以只要子进程退出父进程还在运行但父进程没有读取子进程状态子进程就进入Z状态。
6X-死亡状态
这个状态只是一个返回状态在任务列表里看不到这个状态。因为当进程退出时释放进程所占用的资源时一瞬间就释放完了所以死亡状态看不到。
3.僵尸进程危害
从僵尸状态我们知道了僵尸进程退出时会等待父进程或系统读取其返回码来辨别进程死亡的原因。这就像我们在写代码时main函数的返回值都是0:
#include<stdio.h>
int main()
{
//code
return 0;
}
返回值0就是为了告诉操作系统代码顺利执行结束可以使用echo $?来获取进程最后一次退出时的退出码
当子进程退出而父进程还在运行但是父进程没有读取子进程的退出信息子进程就进入了僵尸状态。
如下面代码zombieProcess.c子进程在打印5次之后退出父进程没有读取子进程的退出信息此时子进程就变成僵尸状态
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)//child
{
int count = 5;
while(count)
{
printf("child PID:%d,PPID:%d,count:%d\n",getpid(),getppid(),count);
sleep(1);
count--;
}
printf("child is quiting\n");
exit(1);
}
else if(id >0)//father
{
while(1)
{
printf("father PID:%d,PPID:%d\n",getpid(),getppid());
sleep(1);
}
}
else//fork error
{
//do nothing
}
return 0;
}
使用监控脚本就可以看到子进程的状态就变成了僵尸状态
僵尸进程危害
1进程的退出状态必须被维持下去因为它要把退出信息告诉父进程如果父进程一直不读取那么子进程就一直处于僵尸状态
2由于进程基本信息是保存在task_struct中的如果僵尸状态一直不退出只要父进程没有读取子进程退出信息那么PCB一直都需要维护。
3如果一个父进程创建了多个子进程并且不回收那么就要维护多个task_struct 数据结构会造成内存资源的浪费
4僵尸进程申请的资源无法进行回收那么僵尸进程越多实际可用的资源就越少也就是说僵尸进程会导致内存泄漏
六、孤儿进程
僵尸进程是子进程先退出但是父进程没有读取子进程的退出信息。
假如父进程先退出子进程后退出此时子进程处于僵尸状态没有父进程来读取它的退出信息此时子进程就称为孤儿进程。
如下代码orphanProcess.c父进程在5秒后终止退出子进程并没有退出
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id ==0)//child
{
while(1)
{
printf("child\n");
sleep(2);
}
}
else//father
{
sleep(5);
printf("father is quiting\n");
exit(1);//父进程5秒后终止
}
return 0;
}
启动监控脚本查看到父进程退出后子进程就变成了孤儿进程但是子进程的PPID变成了1即子进程的父进程变成了1号进程
1号进程是什么进程呢
1号进程是init进程也叫做操作系统进程当出现孤儿进程的时候孤儿进程就会被1号int进程领养当孤儿进程进入僵尸状态时就由1号init进程回收。
为什么孤儿进程会被1号进程领养呢
如果孤儿进程要退出时需要被回收 那么需要一个进程回收它所以孤儿进程被1号init进程领养也就能被1号init进程回收了。
七、进程优先级
1.概念
进程的优先级就是CPU资源分配的先后顺序 即进程的优先权优先权高的进程有优先执行权力。
还有一些其他概念
- 竞争性: 系统进程数目众多而CPU资源只有少量甚至1个所以进程之间是具有竞争属性的。为了高效完成任务更合理竞争相关资源便具有了优先级
- 独立性: 多进程运行需要独享各种资源多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别同时进行运行这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式在一段时间之内让多个进程都得以推进称之为并发
2.为什么要有进程优先级
因为CPU资源是有限的一个CPU只能同时运行一个进程当系统中有多个进程时就需要进程优先级来确定进程获取CPU资源的能力。
另外配置进程优先权对多任务环境的linux很有用可以改善系统性能。还可以把进程运行到指定的CPU上这就把不重要的进程安排到某个CPU可以大大改善系统整体性能。
3. 查看系统进程
使用
ps -l
命令查看系统进程
可以看到
- UID : 代表执行者的身份表明该进程由谁启动
- PID : 代表这个进程的代号
- PPID 代表这个进程是由哪个进程发展衍生而来的亦即父进程的代号
- PRI 代表这个进程可被执行的优先级其值越小越早被执行
- NI 代表这个进程的nice值
4.PRI和NI
- PRI是进程的优先级也就是就是程序被CPU执行的先后顺序此值越小进程的优先级别越高
- NI就是nice值表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行加入nice值后将会使得PRI变为PRI(new)=PRI(old)+nice
- 当nice值为负值时该程序优先级值将变小即其优先级会变高则其越快被执行
- Linux下调整进程优先级就是调整进程nice值
- nice其取值范围是-20至19一共40个级别。
注意 nice值不是进程的优先级是进程优先级的修正数据会影响到进程的优先级变化。
5.使用top命令更改进程优先级
1更改NI值
先运行一个进程使用
ps -l
查看进程号、优先级及NI值比如执行./forkProcess_getpid进程
可以查看到优先级为80NI值为0
在运行top命令之后输入r就会有PID to renice此时输入进程号5255再输入NI值此处设为10
然后查看进程的优先级和NI值优先级变成了90NI值变成了10
说明优先级和NI值已经被改了。由此也能验证
PRI(new) = PRI(old)+nice
PRI(old)一般都是80这就是为什么没有修改NI值之前用ps -al命令查看到的进程的PRI都是80的原因。
2NI的取值范围
现在验证一下NI(nice)的取值范围假如将NI的值设为100
再查看进程的优先级和NI值发现NI值变成了19优先级增加了19
这说明NI的上限就是19那么下限呢此时PID变成了12452
将NI值改为-100
发现NI值变成了-20说明本次 的NI值变成了-20优先级减小了20
这说明NI的取值范围为-20~19一共40个级别。
3NI取值范围较小的原因
因为优先级再怎么设置也只能是一种相对的优先级不能出现绝对的优先级否则会出现很严重的进程“饥饿问题”即某个进程长时间得不到CPU资源而调度器需要较为均衡地让每个进程享受到CPU资源。
八、环境变量
1.概念
环境变量(environment variables)指操作系统中用来指定操作系统运行环境的一些参数。例如在编写C/C++代码的时候在链接的时候从来不知道所链接的动态静态库在哪里但是照样可以链接成功生成可执行程序原因就是有相关环境变量帮助编译器进行查找。
2.常见环境变量
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
3.如何查看环境变量
我们运行可执行程序时都需要在可执行程序前面加上./才能执行
但是在执行系统命令时为什么不需要在前面加上./呢
命令、程序、工具本质都是可执行文件./的作用就是帮系统确认对应的程序在哪里由于环境变量的存在所以执行系统命令时不需要在系统命令前加./
查看环境变量的方法
echo $PATH
系统通过PATH进行路径查找查找规则就是在PATH中先在第一个路径中找找不到就在第二个路径中找再找不到就在第三个路径中找……如果找到了就不往下找了直接将找到的路径下的程序运行起来这就完成了路径查找。即系统执行命令时操作系统通过环境变量PATH去搜索对应的可执行程序路径。
如何让forkProgress执行时不带./跟执行系统命令一样有2种做法
- 把forkProgress命令拷贝到以上6种任意一个路径里不过这种做法不推荐会污染命令池
- 把当前路径添加到PATH环境变量中
平时安装软件就是把软件拷贝到系统环境变量中特定的命令路径下就完成了安装的过程其实就是拷贝的过程。
不能直接把当前路径赋值给PATH否则上面的6种路径就全没了。可以使用export导入环境变量
export PATH=$PATH:程序路径
查找到 forkProcess的路径
添加环境变量
现在在其他路径下也可以执行该可执行程序了比如在家目录下执行
4.和环境变量相关的命令
环境变量的本质是操作系统在内存/磁盘上开辟的空间用来保存系统相关的数据。在语言上定义环境变量的本质是在内存中开辟空间存放key、value值即变量名和数据。
- echo显示某个环境变量值
- export设置一个新的环境变量
- env显示所有环境变量
- set显示本地定义的shell变量和环境变量
- unset清除环境变量
用echo显示某个变量的值
export设置一个新的环境变量 前面已经设置过了
env显示所有环境变量
set显示环境变量
unset清除环境变量
5.环境变量的组织方式
1环境表
每个进程在启动的时候都会收到一张环境表环境表主要指环境变量的集合每个进程都有一个环境表用于记录与当前进程相关的环境变量信息。
环境表采用字符指针数组的形式进行存储然后使用全局变量char** envrion来记录环境表的首地址使用NULL表示环境表的末尾
以前写c代码时main函数可以带2个参数
#include<stdio.h>
int main(int argc,char *argv[])
{
return 0;
}
其中第二个参数argv是指针数组数组元素一共有argc个argc决定了有几个有效命令行那个字符串。可以把命令行参数的细节打印出来
#include<stdio.h>
int main(int argc,char *argv[])
{
int i = 0;
for(i = 0;i<argc;i++)
{
printf("argv[%d] = %s\n",i,argv[i]);
}
return 0;
}
命令行带参数运行
命令行参数数组的元素个数是动态变化的有几个参数就有对应的长度大小
在命令行中传递的各种各样的数据最终都会传递给main函数由main函数一次保存在argv中由argc再表明个数 。
数组结尾是NULL那么可以不使用argc吗不可以原因有两个
- 作为数组传参一般建议把个数带上
- 用户填参数到命令行如果想限定用户输入命令行参数的个数就要用到argc例如
if(argc != 5) { //TODO }
命令行参数的作用在于同一个程序可以用给它带入不同参数的方式来让它呈现出不同的表现形式或功能例如
实现一个程序假如输入参数为o或e就打印hello linux:
inputPara.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main(int argc,char *argv[])
{
if(argc != 2)//输入参数不为2时
{
printf("Usage: %s -[l|n]\n",argv[0]);
return 1;
}
if(strcmp(argv[1],"-l") == 0)//输入第二个参数为-l
{
printf("hello linux! -l\n");
}
else if(strcmp(argv[1],"-n") == 0)//输入第三个参数为-n
{
printf("hello linux -n\n");
}
else
{
printf("hello\n");
}
return 0;
}
输入不同的参数就有不同的执行结果
命令行参数的意义在于指令有很多选项用来完成同一个命令的不同子功能。选项底层使用的就是命令行参数。
假如函数没有参数那么可以使用可变参数列表去获取。
2获取环境变量
- 使用getenv获取环境变量
#include <stdlib.h>
char *getenv(const char *name);
获取PATH、HOME、SHELL这3个环境变量
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("PATH:%s\n",getenv("PATH"));
printf("HOME:%s\n",getenv("HOME"));
printf("SHELL:%s\n",getenv("SHELL"));
return 0;
}
如下
- 使用命令行第3个参数获取环境变量
使用命令行第3个参数env获取环境变量
env1.c
#include<stdio.h>
int main(int argc,char *argv[],char *env[])
{
int i = 0;
for(; env[i];i++)
{
printf("%s\n",env[i]);
}
return 0;
}
结果如下
- 通过第三方变量environ获取
libc中定义的全局变量environ指向环境变量表environ没有包含在任何头文件中所以在使用时要用extern声明。
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
结果如下
6.环境变量的全局属性
环境变量通常具有全局属性可以被子进程继承。
如下代码
geteEnvironment.c
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
printf("pid = %d,ppid = %d\n",getpid(),getppid());
return 0;
}
发现每次运行该程序子进程的ID都不相同但是父进程的ID都相同
命令行上启动的进程父进程都是bashbash的环境变量是从系统里读的系统的环境变量就在系统配置中bash登陆时bash就把系统的配置导入到自己的上下文当中。子进程的环境变量是系统给的也就是父进程bash给的。环境变量一旦导出是可以影响子进程的
环境变量具有全局属性的原因是环境变量是可以被继承的。比如bash创建子进程后子进程又创建了更多的子进程相当于从bash开始一个环境变量被设置所有的子进程全都看到了bash环境变量所有用户都可以获得这个环境变量用这些环境变量做一些搜索查找等的任务gcc和gdb能链接到各种库的原因是他们都是命令都是bash的子进程bash的所有关于库路径的查找头文件查找等各种全局设计都可以被这些命令找到本质上是因为环境变量可以指导编译工具去进行相关查找所以在编译程序时不用带很多选项默认就能找到能够让程序快速完成翻译和调试。
7.本地变量
与环境变量相对的还有本地变量针对当前用户的当前进程生效是一种临时变量退出本次登陆后就失效了。
如下变量value的值在没有退出登录前打印到是5ctrl+d退出登录后
再去echo $value发现value已经失效了
本地变量能被子进程继承吗用env查看发现shell的上下文中是没有的
说明本地变量是不能被继承的只能bash自己用。
现在使用getenv获取这个本地变量的环境变量
getLocalValue.c
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("value = %d\n",getenv("value")) ;
return 0;
}
运行之后发现value变成了0说明刚刚定义的value变量就是本地变量
把定义的value变量用export导成环境变量实际上是导给了父进程bash的环境变量列表
这时候用env查看发现shell的上下文中有了
这说明环境变量已经到给了父进程bashbash中已经有了环境变量./getLocalValue.c运行时它的环境变量信息会继承自父进程父进程现在多了一个环境变量用env就能够获取成功了。
九、程序地址空间
1.程序地址空间分布
C/C++程序地址空间
那么C/C++的程序地址空间是内存吗为了验证它到底是什么可以使用如下代码
printfProcessAddress.c
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int g_UnValue;
int g_Value = 1;
int main()
{
const char *string = "hello world";
char *heap = (char*)malloc(10);
int a = 5;
printf("code address:%p\n",main);//代码区
printf("read only string:%p\n",string);//字符常量区
printf("statck address:%p\n",&string);//栈区
printf("uninit address:%p\n",&g_UnValue);//未初始化全局变量区
printf("Init address:%p\n",&g_Value);//已初始化全局变量区
printf("heap address:%p\n",heap);//堆区
printf("stack address:%p\n",&heap);//栈区
printf("stack a:%p\n",&a);//栈区
return 0;
}
运行之后发现
1代码区的地址0x40057d最小说明在程序地址空间中代码区在最下面
2字符串常量区0x400710次之
3已初始化全局变量区0x60103c次之
4未初始化全局变量区0x601044次之
5堆区0x17e4010、0x17e4030次之两个地址依次增大说明堆是向上增长的
6栈区地址最大并且3个栈地址是依次减小的
先打印了高地址最后打印了低地址 这说明栈是向下增长的。
以上就完整还原了程序地址空间的地址分布。
2.程序地址空间是虚拟地址
先看一段下面的代码子进程在运行过程中修改了全局变量的值
printfFork.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int g_Value = 1;
int main()
{
//发生写时拷贝时数据是父子进程各自私有一份
if(fork() == 0)//子进程
{
int count = 5;
while(count)
{
printf("child,times:%d,g_Value = %d,&g_Value = %p\n",count,g_Value,&g_Value);
count--;
sleep(1);
if(count == 3)
{
printf("############child开始更改数据############\n");
g_Value = 5;
printf("############child数据更改完成############\n");
}
}
}
else//父进程
{
while(1)
{
printf("father:g_Value = %d,&g_Value = %p\n",g_Value,&g_Value);
sleep(1);
}
}
return 0;
}
但是打印时却发现同一个地址g_Value值却不一样
如果写时拷贝访问的是同一个物理地址的话为什么得到的g_Value是不一样的值呢所以程序地址空间使用的不是物理地址而是虚拟地址。
C/C++中用到的都是虚拟地址操作系统不会把物理内存暴露给用户物理地址由操作系统统一管理操作系统负责把虚拟地址转化成物理地址。在计算机刚启动时操作系统没有加载因此计算机就只能访问物理内存操作系统启动之后CPU正常运行就进入了虚拟空间。
所以上面画的程序地址空间分布图不是物理地址而是进程虚拟地址空间。
3.虚拟地址
进程地址空间本质上是操作系统内的一种数据结构类型操作系统让每个进程都感受到自己在独占系统内存资源每个进程都认为自己独占4GB空间。
1mm_struct
在创建一个进程时进程的task_struct结构中包含了一个指向mm_struct结构的指针用来描述进程虚拟地址空间即用户看到的空间。mm_struct中包含装入的可执行映像信息和进程的页表目录指针pgd通过页表将虚拟地址映射为实际的物理地址:
每个进程都认为mm_struct代表整个内存的地址空间。地址空间不仅能够形成区域还能再各个区域中抽象出一个地址因为这个地址是线性连续的。start和end就对应到数组下标下标对应到虚拟地址。task_struct所看到的地址不是物理地址而是虚拟地址。
每个进程都只有一个虚拟空间这个虚拟空间可以被别的进程共享。
那么虚拟地址的作用是什么呢
虚拟地址本质上在软件上为进程画了饼让每个进程都感受到自己在独占资源。无论怎样画饼最终都要能让进程访问地址数据读取并执行代码进行计算。
2页表和MMU
页表是一种数据结构记录页面和页框的对应关系本质是映射表增加了权限管理隔离了地址空间能够将虚拟地址转换成物理地址。操作系统为每个进程维护一张页表。
MMU(Memory Manage Unit)内存管理单元是虚拟地址的整体空间是对整个用户空间的描述。MMU一般继承在CPU当中。
所以进程的各个区包括代码区已初始化区、未初始化区、堆区、栈区、共享区等都是虚拟地址经过页表和MMU映射成对应的物理地址再让进程去访问代码和数据。
3进程地址空间存在的原因
如果进程直接访问内存行不行呢为什么中间要映射呢
这是因为加了一个中间层有利于管理防止进程的不合法行为。如果让一个进程直接去访问物理内存可以访问自己的代码和数据但这个进程也有可能访问修改别的进程的代码和数据甚至有些恶意进程通过非法指针访问操作别的进程的代码和数据这会带来严重问题可能威胁到系统安全。
存在进程地址空间的原因
① 通过添加一层软件层完成有效的对进程操作内存进行风险管理权限管理本质的目的是为了保护物理内存及各个进程的数据安全。
现在在虚拟地址和物理内存中间加了一个页表就相当于加了一个软件层这个软件层是操作系统的代言人页表和MMU在映射时其实是操作系统在映射能不能映射是由操作系统决定的这就能做到权限管理。比如
const char *str = "spring";
*str = "summer";//报错不允许
修改str指向的变量的值时是不被允许的。因为str是栈上的局部变量但是spring在字符常量区不可以修改因为操作系统给你的权限只有r读权限。这就是为什么代码区内容是不可以修改的字符常量区内容也不能修改的原因。因为页表是有权限管理的给代码区和字符常量区分配的权限是r读权限。所以str指向的是虚拟地址当要对*str进行写入的时候访问的也是虚拟地址这就需要操作系统进行虚拟地址和物理地址之间的转换但是看到*str的权限是r读不能写入就把进程崩溃掉。
② 将内存申请和内存使用的概念在时间上划分清楚通过虚拟地址空间来屏蔽底层申请内存的过程达到进程读写内存和操作系统管理操作进行软件层面上的分离的目的让应用和内存管理解耦。
假如某个进程申请1000个字节那么它能够马上使用这1000个字节吗不一定可能会存在暂时不会全部使用甚至暂时不使用的情况。站在操作系统角度如果把这1000个字节的空间马上给这个进程那么就意味着本来可以给别人马上用的空间现在却被你闲置着。因此操作系统不会马上给这个进程分配1000个字节的物理内存空间但是在进程虚拟地址空间上是批准了的。当进程马上要使用这1000个字节的空间时进程就会告诉上层说已经申请1000个字节的空间了准备要访问了这是操作系统就会在物理内存中申请1000个字节的空间这次的申请空间是透明的然后把这实际的1000个字节的空间和进程申请的虚拟空间建立映射关系。
③ 站在CPU和应用层的角度进程可以统一看作使用4GB空间每个区域的相对位置是确定的目的是让每个进程都认为自己独占着系统资源。
程序的代码和数据是要加载到物理内存的操作系统需要知道main函数的物理地址。如果每个进程的main函数物理地址都不一样那么对于CPU来说执行进程代码时都要去不同的物理地址找main函数这样很麻烦。每个进程的main函数物理起始地址可能都不相同但是有了进程地址空间以后就可以把main函数的物理起始地址通过页表和MMU都映射成同一个虚拟空间地址这样就把这一个虚拟空间地址和各个进程的物理地址建立起了映射关系。假如还要运行其它进程就可以把其他进程的main函数其实地址映射到那个虚拟空间地址。这样CPU在读取进程的时候main函数起始代码统一都从同一个起始位置去读每个进程的main函数入口位置都可以找到。
另外数据和代码可能在物理内存中不连续而页表通过映射的方式把所有的代码区、已初始化全局数据区、未初始化全局数据区等映射到虚拟地址空间上时可以把它们映射到连续的区域形成线性区域。
4.写时拷贝
在printfFork.c的代码中让子进程运行5秒第3秒的时候把g_Value的值改掉了所以同一个地址子进程打印了2次1后面的都是5了父进程一直打印1原因就是每个进程都有自己的页表地址是虚拟地址而不是物理地址。
程序刚开始运行时只有一个进程即父进程 父进程的pcb指向父进程的地址空间全局变量定义出来的时候子进程没有forkg_Value对应已初始化区域的定义的全局变量经过页表映射到物理内存上的g_Value
当fork创建子进程时以父进程为模板为子进程创建新的pcb、地址空间和页表 。子进程把父进程的大部分内容都继承下来了比如地址空间子进程的地址空间、页表也都和父进程一样所以创建子进程后一开始子进程也指向了父进程的g_Value
在第3秒的时候子进程修改了g_Value的值操作系统并没有让子进程直接把值改了因为进程具有独立性互不干扰。修改时发生写时拷贝给子进程重新开辟一块物理空间把g_Value变量值拷贝进来再重新建立子进程的虚拟地址到物理地址之间的映射
因此看到的子进程和父进程打印的地址是一样的是因为虚拟地址是一样的。值不一样的原因是在物理内存上本来就是不同的变量。