1.预备知识
1.1冯诺伊曼体系结构
- 存储器:内存(掉电已失)(较快)
- 运算器+控制器+其他 = 中央处理器(CPU(寄存器:CPU内部的高速存储器,速度快,数量少),芯片):含有运算器和控制器等(快)
- 外设:磁盘(外存)永久性储存能力 输入/输出设备(较慢)
- 输入设备:包括键盘, 鼠标,扫描仪, 写板,摄像头等
- 输出设备:显示器,打印机等
- 输入输出设备:磁盘&&网卡
1.2计算机的内存与CPU
CPU其实很笨,只能接受别人的指令,别人的数据-------》执行别人的指令,计算别人数据的目的
先要认识别人的指令(有自己的指令集)
我们写代码,经过编译成二进制可执行程序,才能被CPU计算
CPU读数据从内存中拿(在数据层面只和内存打交道,为了提高整机的效率)
内存数据永远在磁盘上,在CPU要之前,已经搬到了内存中(开机时加载,内存相当于缓存)
加载啥数据由操作系统决定
计算机就算硬件与软件的完美结合
1.3IO过程(INPUT&&OUTPUT)
IO过程就是由内存到外设的过程
1.4结论
这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
内存写出,也一定是写到外设中去
能解决的问题:程序为啥要运行必须加载到内存,因为CPU要执行我的代码,只能从内存中读取(体系结构规定!)
外设和CPU在控制层面上,有交互,这里是中断
CPU可以进行数术运算和逻辑运算
1.4操作系统
操作系统的作用:向下管理硬件,向上提供接口
Linux子系统
1.进程管理:管理进程的创建、调度、销毁等
2.内存管理:管理内存的申请、释放、映射等
3.文件系统:管理和访问磁盘中的文件
4.设备管理:硬件设备及驱动的管理
5.网络协议:通过网络协议栈(TCP、IP...)进行通信
2.进程
进程:一个运行起来的(加载到内存)的程序
在内存中的程序叫进程
进程和程序相比,进程具有动态属性
如何管理这些进程呢?先描述再组织------------》PCB
task_struct | PCB
在Linux中描述进程的结构体叫做task_struct。
课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
进程的属性
改进程对应的代码和属性地址
struct task_struct *p1 = malloc(struct task_struct);
p1->...= xxx
p1->addr = 代码和数据的地址
所谓对进程的管理就是对PCB结构作管理(对链表的增删查)
struct task_struct 内核结构体 ------》 创建内核对象 -------》 该结构将你的代码和数据关联起来 --------》完成了:先描述再组织的工作
进程 = 内核数据结构+进程对应的磁盘代码
查看进程(1)
ps ajx | grep "myproc" //grep行过滤
可以grep -v grep//把grep进程过滤掉
ps ajx | head -1 //显示标题
ps ajx | head -1 && ps ajx | grep "myproc"
查看进程(2)
ls /proc //进程可以被当作文件来查看
查看进程详细信息
进入该进程所在目录然后ll
会出现exe,为该可执行程序在磁盘上的位置
当把进程可执行程序删除,进程还在运行,但exe在闪,提示你该文件已经被删除
说明该程序被加载到内存,已经和磁盘没有关系了,也有特殊情况。
杀掉进程kill -9 PID号
2.1如何获得进程的pid
系统调用 man 2 gitpid
gitpid
pit_t是整数类型的
父进程常见调用
man getppid //获取父进程ID
父进程为bash(这就是shell对用户的指令进行创建子进程)
你的程序是以子进程执行的,不怕你有BUG(王婆派实习生去说媒)
当把bash执行kill -9 32114时,系统就会崩溃
每当操作系统开机的时候,就会指派一个SHELL(bash)
--------->命令行上执行的命令,没有特殊情况,父进程都是BASH
2.2创建子进程
子进程创建fork
fork() //创建子进程,是一个函数-----函数执行前:只有一个父进程------函数执行后: 父进程+子进程
函数执行时出现两个子进程
第一个是第二个的父进程
第一个的父进程是bash
函数执行时出现
fork的返回值,给父进程返回自身(子进程)id,给子进程返回0
同一个id,有两个返回值?????????为什么???????(写实拷贝)
fork之后,会有父进程+子进程在执行后续代码
fork后续代码是父子进程共享
通过返回值不同,让父子进程执行后续共享的代码
2.3进程的状态
不同的状态就是task_struct在不同的队列中,等待不同资源
2.3.1系统层面
宏观概念
1.os
struct dev_keyboard
{
task_struct *wait_queue;
属性
}
struct dev_display
{
task_struct *wait_queue;
属性
}
struct 网卡
{
task_struct *wait_queue;
属性
}
struct 磁盘
{
task_struct *wait_queue;
属性
}
。。。。
struct task_struct
{
进程属性 -------》.exe文件
}
进程的不同状态为了应对不同的运行场景。
struct runqueue《--------CPU找到要执行的进程(1个CPU一个运行队列)
{
task_struct *head;
//其他属性
}
所谓的进程运行,就是让进程task_struct进入队列(先进先出)
在运行队列里的进程PCB就称为运行状态,不是进程再次运行,才是运行状态。
状态是进程内部的属性-------》task_struct-------->int(1:run 2:stop 3:hup 4:dead.....)
2.驱动
3.硬件:键盘 显示器 网卡 显卡 磁盘
CPU很快,硬件很慢,但进程或多或少都要访问他。但这些硬件数量也很少。当这写硬件被占用的时候,这些进程也只能等待。
不要只意味,你的进程只会等待(占用)CPU资源,你的进程随时访问外设资源。
每一种外设也有自己的task_struct *wait_queue属性,当外设被占用,CPU的运行队列,会将该进程放到外设(磁盘)队列中,当外设开始执行该队列的时候,再通知CPU来执行。 把该进程再放到运行队列中去。
- 就绪(新建)状态:就是该进程被创建好。
- 运行状态(等待CPU资源): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- 阻塞状态(等待外设资源):CPU在执行进程时,需要访问外设,而外设在被占用,该进程所处的状态为阻塞状态。
- 挂起状态:当多个阻塞的进程时,CPU为了腾空间,将部分进程的代码和数据重新写入磁盘(唤出),当运行空间充足。进程再次被执行,重新将数据和代码加载到内存(唤入)。
阻塞不一定挂起,但挂起一定阻塞。
2.3.2Linux的底层实现
/* * 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阻塞状态): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。(printf ---》显示器(外设)-----》慢-----》 等显示器就绪--------》 CPU花很长时间。%99都在等待%1在执行代码)
- D(深度睡状态)磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,无法被操作系统杀掉,只能通过断电,或进程自己醒来,结束。
- 当进程A在将1000条用户信息,写到磁盘上时,磁盘开始慢慢悠悠的开始写,进程就在等待,这时候操作系统内存吃紧,已经不能靠挂起进程来解决了,这时候就会出现杀掉进程来解决,就看见进程A在闲着,于是把它杀了,这时候磁盘就反馈写入失败了,进程说操作系统把我给杀了,甩锅给操作系统,操作系统就说我在更好的分配资源。于是人们就给进程A一个免死金牌,操作系统没法杀该进程,这就是深度睡眠状态,当进行大量文件IO操作时会出现。
- 挂起状态:看不到,这是操作系统内部执行的状态。
- T停止状态(stopped阻塞状态): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。kill -19 进程编号(19代表暂停)(18继续)
+号代表是前台进程,没有+的叫后台进程(不能用CTRL+C终止,可以kill -9 进程编号)。
- t追踪停止状态(tracing stop): 当该进程在被GBD调试的时候,代码中有断点就会出现该状态。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
- N:低优先级的进程
- L:有内存分页分配并锁在内存中
2.4(zombie)僵尸进程
进程被创建出来是为了完成任务---------1.要知道他完成的如何?2.可以不关心结果(你可以不要,但我不能没有)
进程退出的时候,不能立即释放该进程占有的资源。保存一段时间,让父进程或OS读取。在结束到等回收的状态就成为僵尸状态。
进程退出了,但是没有被回收(父进程 os)=====>创建子进程,让父进程不要退出,且什么也不做,让子进程正常退出。
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork"); // err
return 1;
}else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}else{ //child
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);//exit(1);
}
return 0;
}
while:;do ps ajx | head -1 && ps ajx | grep mypross | grep -c grep; sleep 1;done
2.4.1僵尸进程危害
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏?是的!
- 如何避免?后面的等待进程结束
2.5不可被杀的进程 Z D X
2.6孤儿进程
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收。
- 子进程被操作系统会被操作系统领养,就称为孤儿进程,状态变成S(后台进程),只能killl -9杀掉。
- 如果子进程不被领养,就会成为僵尸进程。
nclude <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
当用kill parent进程的时候,并没有出现僵尸进程,这是因为创建parent进程的父进程是bash,他会回收资源。
当父进程死掉,child进程的parent直接变成1。
1号进程对应的就是操作系统
2.7进程的优先级
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
什么是优先级?
能做,先做还是后做的问题。
为什么有优先级?
资源太少了
Linux优先级特点========很快
优先级的本质
PCB里面的整数(一个或几个)
在linux或者unix系统中,用ps –la命令则会类似输出以下几个内容:
我们很容易注意到其中的几个重要信息,有下:
- UID : 代表执行者的身份
- PID : 代表这个进程的代号
- PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI :代表这个进程可被执行的优先级,其值越小越早被执行
- NI :代表这个进程的nice值(越NICE,优先级越低)
2.8PRI vs NI
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice (LINUX下PRI(old) = 80)
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是-20至19,一共40个级别。
普通用户调整 NI 值的范围是 0~19,而且只能调整自己的进程。
普通用户只能调高 NI 值,而不能降低。如原本 NI 值为 0,则只能调整为大于 0。
只有 root 用户才能设定进程 NI 值为负值,而且可以调整任何用户的进程。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进
程的优先级变化。
可以理解nice值是进程优先级的修正数据
2.8.1查看进程优先级的命令
top
进入top后按“r”–>输入进程PID–>输入nice值
2.9其他概念
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰(父子进程也是相互独立的)
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
2.10.进程之间的切换
CPU中有寄存器(一套寄存器),其中有PC寄存器(程序计数器,用于存储当前取址指令的地址),LR寄存器(链接寄存器),SP寄存器(栈寄存器),CPSR寄存器(状态寄存器)。其中又有指令流水线,3级流水线(取址,译码,执行)。
当我们进程在运行的时候一定会产生许多临时数据。这份数据属于当前进程,CPU内部虽然存在一套寄存器硬件,寄存器里面保存的数据是属于当前进程的。寄存器(硬件)!= 寄存器内部数据
进程在运行的时候占用CPU,进程不是一直要占有到进程结束,如while(1)
进程在运行的时候,都有自己的时间片!所以都有跑一段时间就离开的情况。当兵(学习保留学籍)上下文保护,当兵回来,恢复学籍(上下文恢复)。这就相当于进程切换。
进程在切换的时候,临时数据保存起来(上下文保护),放到PCB(暂时这样理解)中,再放另一个进程。当该进程会来的时候数据重新回来(上下文恢复)
在任何时候,CPU里面的的寄存器里面的数据,看起来是在大家都能看到的寄存器上,但是,寄存器的数据,只属于当前运行的进程。
寄存器被所有进程共享,寄存器内的数据,是每个进程各自私有的-------上下文数据
2.11进程地址空间
地址空间:是OS内实现进程管理,所设计的一种虚拟化解决方案,通过地址空间,可以让每个进程都独占系统资源。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define USER "USER"
int global_val=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("child,pid:%d,ppid:%d | global_val:%d,global_val:%p\n",getpid(),getppid(),global_val,&global_val);
sleep(1);
cnt ++;
if(cnt == 10)
{
global_val = 300;
printf("gaila.........\n");
}
}
}
else
{
while(1)
{
printf("father,pid:%d,ppid:%d | global_val:%d,global_val:%p\n",getpid(),getppid(),global_val,&global_val);
sleep(2);
}
}
return 0;
}
global_val被改后,父子进程的值发生了改变,因为父子进程是独立的,但两个global_val的地址是一样的,说明这里的地址不是物理地址-------》曾经我们学习的语言指针对应的地址,不是物理地址。是虚拟地址(线性地址)【逻辑地址】,C/C++打印的地址都是虚拟地址。
上述结果产生的原因是:
在创建子进程的时候,会将父进程的所有东西进行以下拷贝。但当要修改其中的数据的时候。OS保证进程之间的独立性-----》就对改数据,重新在物理地址上进行拷贝。要修改也是只改重新拷贝好的数据,对与虚拟地址空间没有改变。------------》这样的技术就叫写时拷贝。
OS,为了保证进程的独立性,做了很多工作!!!!-----》通过地址空间,通过页表,让不同的进程,映射到不同的物理内存处
进程的独立性:1.独立的内核数据结构。2.不同进程的数据进行分离
- 地址空间的存在可以更方便地进行进程和进程的数据代码的解耦,保证了进程之间的独立性
- 让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便编译器也以统一的视角进程编译代码。(方便使用,编译完,就能使用)
其实每个进程都会认为自己独占系统所有空间,(事实不是)-------》(就如你在睡觉,被别人搬开,睡醒之前再诺回来,你会以为你以一直在床上)
地址空间的本质:是内核的一种数据结构:mm_struct
1.地址空间描述的基本空间大小是字节
2.32位-----》2^32次方个地址
3.2^32字节4*2^30=4GB空间范围
4.每个字节都要有唯一的地址------》要表示2^32个地址-----》要保证地址的唯一性-------》用32位的数据就可以了0000 0000 0000 0000 0000 0000 0000 0000
所以地址从下到上,是从低地址到高地址排列0000 0000 0000 0000 0000 0000 0000 0000----》ffff ffff,这叫编制。
5.堆区,栈区怎么解释呢?(尺子+桌子,给桌子画了个线,这就是区域划分)
struct Destop
{
//给男生画区域
unsigned int nan_start;
unsigned int nan_end;
//给女生画区域
unsigned int nv_start;
unsigned int nv_end;
}
struct Destop d = {1,50,51,100}; //每一个数值就可以代表桌面的位置
对区域进行调整
struct Destop d = {1,45,55,100};
对女孩的区域扩大
struct Destop d = {1,30,31,100};
同理,计算机的区域划分,就是对2^32个地址进行划分(unsigned int) ,地址调整也是对start和end进行修改
struct mm_struct
{
unsigned int code_start,code_end;
unsigned int data_start, data_end;
unsigned int heap_start, heap_end;
unsigned int stack_start, stack_end;
....
}
首先申请*mm = malloc(struct mm_struct);
区域的地址,称为虚拟地址,之间的每一个数就代表一个地址(2^32)
区域调整,就是在修改end和start ------>在定义局部变量,malloc new堆空间------》扩大栈区或堆区
函数到用完毕,或free地址空间的时候------》缩小栈区或堆区
内存在和磁盘在进行输入输出的时候叫IO,操作系统在运行IO的时候,基本单位是4KB,1KB=1024bit
磁盘的可执行文件被加载的内存中去(物理内存)跟虚拟地址空间的关系是:
可以把内存看作一个大数组 struct page_mem[4GB/4KB]------>对应4GB/4KB个页---》要访问一个页,只要知道页的起始地址+页偏移就可以了
假设虚拟地址中的一个地址(0x1234 5678)被保存在了物理地址(0x1111 2222),在页表中,左侧保存虚拟地址,右侧保存物理地址,这样两者就存在了影射关系。
C中&c ----->就是虚拟地址,再查页表,找到对应的物理地址,访问空间和数据,对数据进行修改,就完成了写入。
这里的页表其实是多级页表,是个树状结构
这里的虚拟地址空间也叫线性地址(按线性排列的)
为什么存在地址空间?
- 如果让进程直接访问物理内存,万一进程越界非法操作,非常不安全。
(压岁钱,放到父母那,我给你管着,要的时候,给你,当你要买不好的东西时候,就不给你,这样防止你被骗,变相的保护你)
页表不仅仅只做影射,还会变相的保护你,当你访问某个空间,把需求发给页表,页表会对不合理请求进行拦截。(OS在执行,所有进程都要遵循)
重新理解地址空间
在我们的可执行程序里面有没有地址呢?(在没有加载到内存的时候?)(汇编的时候代码就已经有地址了(逻辑地址),函数是有地址的)
虚拟地址空间,不仅仅是OS在遵守。编译器也在遵守。编译器在编码的时候,就是在按照虚拟地址空间的方式,就对我们的代码和数据进行编码。这些编译后的程序中的代码是有地址的,能够实现函数的调用和跳转,是虚拟地址。
代码要在物理内存中保存,只要加载到物理内存中,此时代码也就拥有了物理地址。
当程序被加载到了物理内存的时候,该程序对应的指令和数据,就天然的具有了物理地址。将代码对应的(虚拟地址)内部地址,直接放到虚拟地址中去。
当我们CPU的pc指针访问的是代码的虚拟地址。通过页表映射,可以找到代码的物理地址。main函数的地址被我们调到CPU中,再去访问代码区其他函数的时候,还是调用虚拟地址。CPU就没见过物理地址
这就是我们在编译程序的时候选择32/64位编!!!!!!!!!!!
3.进程的控制
3.1进程的创建
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
pit_t是整数类型的
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
fork调用失败的原因可能是:
1.系统中太多个进程
2.实际用户的进程超过了限制
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程(代码共享)
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork常规用法
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。
2.一个进程要执行不同的程序,根据fork的返回值不同。
Linux中所有的进程都会被放到一个哈希表中
进程控制块 struct task_struct *hash_table[1000000] //结构体指针数组
pid作为健值去访问这个数组。他的返回值就是进程控制块的起始地址。
3.1.1fork的两个返回值
fork是OS的函数,在fork函数中,会进行子进程的创建,在return前,而子进程的创建早在函数中创建好了,这个子进程可能在OS的运行队列中,准备被调度了。这是就会出现两个执行流,fork之后代码共享,就产生了两个return。
- 父进程返回子进程pid,给子进程返回0,孩子找父亲的是有唯一性的。这就给父进程返回子进程的pid的原因。
3.1.2写实拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写实拷贝的方式各自一份副本。
3.2.进程终止
3.2.1进程退出码
1.why main里面会有return 0;
这个return 0,是进程退出的时候,对应的退出码。
这个退出码可以标定进程执行的结果是否正确。
//写代码就是为了完成某件事情,我是如何得知我的任务跑的如何呢?
进程退出码--------》echo $?
./mytest ------->运行代码 echo $? ------->永远记录最近一个进程在命令行中运行完毕时对应的退出码(main ---->return) 再执行echo $?----->返回的是0,因为上一个echo $? 也是一个进程。
2.以后如何设置return的进程退出码呢?
1.不关心进程退出码-------》return 0;
2.关心进程退出码的时候-------》特定的错误,返回特定的错误
成功用0表示,非0表示失败(1野指针,2逻辑错误)数值对人不直观
一般而因,退出码都有文字描述strerror,也可以自己定义
//查看strerror对应的错误信息 #include <stdio.h> #include <string.h> int main() { for(int i = 0 ;i< 200 ;i++) { printf("%d:%s\n",i,strerror(i)); } return 0; }
#include <stdio.h> #include <string.h> #include <stdlib.h> int main() { printf("hello world\n"); exit(12); return 0; }
3.2.2进程退出的情况
- 代码运行结束,结果正确------return 0;
- 代码运行结束,结果不正确-----return !0;
- 代码没跑完,程序异常了-----退出码无意义(除0,越界,野指针)
3.2.3.进程的结束方式
1.main函数结束
2.任意地方exit(int status)-----终止这个进程,这里的status就是进程退出码
3._exit(int status)-----终止这个进程,这里的status就是进程退出码
#include
#include
void exit(int status);
void _exit(int status);
void _Exit(int status);
结束当前的进程并将status返回
exit结束进程时会刷新(流)缓冲区
1.exit() vs _exit()
exit为库函数,在系统调用之上的,底层就是调用_exit()
_exit为系统调用(操作系统层调用)
在执行
printf("hello world"); //没有\n
sleep(2);
exit(1);
执行会出现,当系统终止的时候才打印出hello world
将exit换成_exit
执行会出现不打印hello world,直接结束
原因:这是因为hello world在缓冲区,没有被刷新到屏幕上,当线程结束的时候,执行exit(1)时,是调用底层的_exit(),主动刷新缓冲区,会刷新到屏幕上,而在执行_exit(1)时,系统直接关闭线程,不会再打印。调用
3.3进程等待
- Z状态:僵尸状态:子进程退出,父进程没有获取到子进程的状态。
- 通过进程等待的方式,解决僵尸进程。
1.进程等待必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的目的:回收子进程的资源,获取子进程退出的信息
3.3.1.进程等待的方法
1.wait等待成功(返回值为子进程Pid,失败为-1)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id == 0)
{
int cnt =10;
while(cnt)
{
printf("son,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
exit(0);
}
sleep(15);
pid_t id2 = wait(NULL);
if(id2 >0)
{
printf("wait success %d\n",id2);
}
sleep(5);
}
2. waitpid()等待获取子进程状态信息(返回值为子进程Pid,失败为-1)
pid_t id2 = waitpid(id,&status,0); //参数:等待进程id,等待状态(子进程的进程退出码),0阻塞式等待
pid_t id2 = waitpid(id,&status,WNOHANG); //WNOHANG:非阻塞等待
作用:
1.让OS释放子进程的僵尸状态
2.获取值进程的退出结果
3.在等待期间,子进程没有退出的时候,父进程只能在阻塞状态。
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
- Pid=-1,等待任一个子进程。与wait等效。
- Pid>0.等待其进程ID与pid相等的子进程。
status:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)options:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
if(WIFEXITED(status)) { printf("子进程正常退出,进程退出码:%d\n",WEXITSTATUS(status)); } else{ printf("子进程异常退出\n") }
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id=fork();
if(id == 0)
{
int cnt =10;
while(cnt)
{
printf("son,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
exit(10);
}
//sleep(15);
int status = 0;
pid_t id2 = waitpid(id,&status,0); //参数:等待进程id,等待状态(子进程的进程退出码),阻塞式等待
if(id2 >0)
{
printf("wait success %d,status:%d\n",id2,status);
}
sleep(5);
}
执行上述代码的时候会发现:
这里的status=2750;
注意:这里的 status不是被整体使用的,有自己的位图结构
进程退出的三种方式
1.运行完
- 代码运行结束,结果正确------return 0;
- 代码运行结束,结果不正确-----return !0;
2.异常
- 代码没跑完,程序异常了-----退出码无意义(除0,越界,野指针)
通过status要体现上面的三种情况。
status不是被整体使用的,有自己的位图结构
次第8位退出状态,第7位终止信号-----》表示是否正常结束。
退出状态就是进程退出码
kill -l就可以看到进程退出的信号
所以退出的时候就要这样看
printf("wait success %d, sig_number:%d, child_exit_code:%d\n" ,id2,(status&0x7F),(status>>8)&0xFF);
总结:
1.进程退出会变成僵尸------会把自己的退出结果写在自己的task_struct(pcb)
2.wait/waitpid是一个系统调用--------》OS有资格和能力去读取子进程的task_struct(pcb)
3.从退出子进程的task_struct(pcb)中获取
3.非阻塞VS阻塞
用打电话来类比
1.张三不挂电话-----》检测李四的状态=====》阻塞
2.张三不时的打电话李四-----》本质,状态检测,如果李四没有就绪,张三直接返回====》非阻塞(每次打电话都是非阻塞等待,多次非阻塞等待称为轮询)
3.把打电话的状态叫做系统调用wait/waitpid,张三----》父进程,李四------》子进程。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
int main()
{
pid_t id=fork();
assert(id >= 0);
if(id == 0)
{
int cnt =10;
while(cnt)
{
printf("son,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
sleep(1);
}
exit(0);
}
int status = 0;
while(1) //非阻塞轮询等待
{
pid_t ret = waitpid(id,&status,WNOHANG);//WNOHANG:非阻塞等待----》子进程没有退出,父进程检测,立即返回
if(ret == 0)
{
//子进程没有退出,我的waitpid没有失败,仅仅是检测到子进程没有退出。
printf("wait done,but child is runing....\n");
}else if(ret >0)
{
//1.waitpid调用成功&&子进程退出了
printf("wait success,exit code :%d, sig :%d\n",WEXITSTATUS(status),status&0x7F);
}else
{
//waitpid调用失败
printf("wait pid failed\n");
}
sleep(1);
}
}
为什么要非阻塞等待
不会占用父进程的所有精力,可以在轮询的时候,可以干点别的。
3.4进程的替换
3.4.1预备知识
1.环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
要在执行一个指令,要先找到程序。
./mypross -> ./表示当前路径----》找到该程序
如果想让./mypross 不加./执行
方法1
sudo cp mypros /usr/bin/
不推荐,因为你写的代码是没有经过测试的,sudo rm /usr/bin/mypros
为什么在/usr/bin文件夹下,就可以直接被找到,因为存在环境变量。
方法 2
将当前路径添加到环境变量去
export PATH = /xxxx/xxxxx/xxxxxx(不敢这样,会覆盖原来的环境变量)
export PATH=$PATH:/XXX/xxx/xxx/xxx (这样可以)
环境变量:
echo $HOME //当前用户的工作目录
echo $HOSTNAME //主机名
echo $LOGNAME //当前登陆的用户
echo $HISTSIZE //历史命令
env直接查看系统所有的环境变量
操作系统在启动BASH的时候就预先给我们设置好,未来我们要用到的变量,这就是环境变量。
命令行也是可以添加环境变量的,命令行也是进程,运行时可以对空间进行动态调整,就可以保存数据。变量的本质就是数据。
2.环境变量的操作
getenv(获得环境变量man 3 getenv) vs putenv(改变环境变量)
myval = 1234567
echo $myval
当执行env | grep myval时,是什么也没有的
当用set | grep myval 时,会出现
set既显示环境变量,又显示本地变量
unset myval //取消环境变量
这里的myval为本地变量(就像C中的局部变量)
export myval //可以将本地变量转为环境变量,当执行./myval时会输出myval = 1234567
unset myval //取消环境变量
当执行./myval 的时候 -----bash(系统进程)会把myval变成一个进程(bash的子进程)(其实是fork())------环境变量具有全局属性------是会被子进程继承下去(为什么被继承,为了不同的应用场景)『如认证身份,获取身份信息』本地变量只会在当前进程(bash)内有效。
3.命令行参数
int main()函数可以有几个参数(3个)
int argc,char* argv[ ] char *env[ ] //命令行参数(整型,指针数组(字符串),环境变量指针数组(字符串)),他的作用:
程序名 选项,依次传给argv的,程序名+选项一共有几个,argc就等于几。
//ls -a -b -c -d =======>这整体是个大字符串---》“ls” “-a” “-b” “-c” “-d” NULL,argv存储的就是该字符串。
这些是SHELL和系统来做到操作,将命令行解析成上面的样子。
其中环境变量指针数组跟指针数组一样,以NULL 结尾
for(int i= 0;env[i];i++) { printf("env[%d]:%s\n",i,env[i]); }
当然,main可以不传环境变量,可以用getenv获取值
也可以通过extern char **environ(第三方环境变量,系统给的,指向环境变量指针数组的指针,二级指针)
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
4.三种获得环境变量的方法
- getenv ---------->推荐使用,直接获得值
- char* env[]
- extern char**environ
3.4.2进程替换的方式
进程替换就是将指定的程序加载到内存中去,让指定进程进行执行。
1.找到程序 2.如何执行新程序
1.exec函数
进程调用exec函数族执行某个程序
- 进程当前内容被指定的程序替换
实现让父子进程执行不同的程序
父进程创建子进程
子进程调用exec函数族
父进程不受影响
进程号不会改变,最后一定要加NULL
其实有六种以exec开头的函数,统称exec函数族:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);//调用失败返回-1,成功没有返回值,这是因为,成功就和后面的代码没有关系了。只要返回一定错了。...为可变参数列表
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, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
创建子进程的目的?
- 让子进程执行父进程代码的一部分---》执行父进程对应磁盘代码中的一部分。
- 想让子进程执行一个全新的程序----》让子进程想办法,加载磁盘上指定程序,执行新程序的代码和和数据(进程的程序替换)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
int main()
{
printf("process is running....\n");
pid_t id = fork();
assert(id >=0);
if(id == 0)
{
execl("/bin/ls","ls","-a","-l",NULL);
exit(1);
}
else
{
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0) printf("wait success,code_exit:%d,sig:%d\n",(status>>8)&0xff,status&0x7f);
}
return 0;
}
上述代码的子进程就只执行ls -a -l的指令,然后退出。
程序替换的原理就是:
将指定程序的代码和数据加载到指定的位置和数据! 覆盖自己的代码数据。
进程替换的时候,没有创建新的进程。
(最后的输出语句没有执行)的原因是:execl执行完毕的时候,代码已经完全被覆盖,开始执行新的代码了。
虚拟地址空间+页表保证进程独立性,一旦有执行流替换代码和数据,就会发生写实拷贝。
1.l:list
int execl(const char *path, const char *arg, ...);//调用失败返回-1,成功没有返回值,这是因为,成功就和后面的代码没有关系了。只要返回一定错了。
execl("/bin/ls","ls","-a","-l",NULL);
2.l:list p:path //不用告诉我路径,只说是谁就行,自动在环境变量中找可执行程序。
int execlp(const char *file, const char *arg, ...);
execlp("ls","ls","-a","-l",NULL);//参数1执行谁,2执行命令
3.l:list e:自定义环境变量
int execle(const char *path, const char *arg, ...,char *const envp[]);
mybin内容 printf("PATH:%s\n",getenv("PATH")); printf("PWD:%s\n",getenv("PWD")); printf("MYPATH:%s\n",getenv("MYPATH")); @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ char* const envp[] = {(char*)"MYPATH=111122233",NULL} //上面是自定义环境变量 execle("./mybin","mybin",NULL,envp); extern char**environ execle("./mybin","mybin",NULL,environ); //environ为系统环境变量,默认环境变量你不传,子进程也能获取 当既想要系统的环境变量,也想要你自己的环境变量就可以使用putenv 这就是为什么有的exec*不加e 将自己的变量加入环境变量 put((char*)"MYPATH=111122222333");
4.v:vector可以将所有的执行参数放入数组中,统一传入,不用使用可变参数传参了。
int execv(const char *path, char *const argv[]);
char * const argv_t[] = {"ls", "-a","-l",NULL}; execv("/bin/ls",argv_t);
5.v:vector p:path
int execvp(const char *file, char *const argv[]);
char * const argv_t[] = {"ls", "-a","-l",NULL}; execvp("ls",argv_t);
int execve(const char *path, char *const argv[], char *const envp[]);
这个才是真正的系统调用,其他的都是封装,为了让你有更多的选择性。
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册 第2节,其它函数在man手册第3节。
程序替换,可以使用程序替换,调用任何后端语言对应的可在执行程序====程序。
为什么我们的程序要加载到内存里,因为冯诺伊曼体系结构,必须要CPU运行代码,如何让加载呢?--------》 Linux用的是exec*系列的接口,exec加载器。
先加载execle还是执行main(int argc,char* argv[],char* env[])函数呢?先加载。
int execle(const char *path, const char *arg, ...,char *const envp[]);
main函数的参数就是从execle中来的,你不传,系统也会给你传。
2.system
#include
int system(const char *command);
成功时返回命令command的返回值;失败时返回EOF
当前进程等待command执行结束后才继续执行
与exec不同的是,他不会替代程序,只是向进程中插入
#include <stdio.h> #include <stdlib.h> int main(int argc, char const *argv[]) { system("ls -a -l ./"); printf("没有被替换\n"); return 0; }
4.进程常用命令
4.1PS命令(快照系统进程)
ps -ajx //显示进程的详细信息,包括进程的作业控制信息以及与该进程相关的其他进程。
ps -elf 普通使用标准查看
ps -aux BSD标准查看
ps 命令详细参数:
-e:显示所有进程
-l:长格式显示更加详细的信息
-f 全部列出,通常和其他选项联用
4.2top命令(实时任务管理器)
shift+>向后翻页
shift+
top -p PID查看某一进程
4.3nice改变正在运行进程的优先级&& 用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值
nice指定的优先级运行进程
nice -n NI值 要启动的进程
renice改变正在运行进程的优先级
renice [NI值] PID
4.4kill终止进程命令
使用kill命令终止进程
–kill [-signal] PID
signal是信号,PID是进程号
kill 命令向指定的进程发出一个信号signal,在默认的情况下,kill 命令向指定进程发出信号15,正常情况下,将杀死那些不捕捉或不忽略这个信号的进程
kill -l查看信号
4.5jobs 查看后台进程
程序+&运行起来的状态为后台运行
也可以使用ctrl+Z让进程进入T状态,进入后台,用jobs可以查看
4.6bg 将挂起的进程在后台运行
4.7fg 把后台运行的进程放到前台运行