目录
冯诺依曼体系结构
操作系统
设计操作系统的目的
操作系统的管理
进程
PCB
fork
进程状态
进程状态查看
僵尸进程
孤儿进程
进程优先级
查看、修改进程优先级命令
竞争、独立、并行、并发
进程切换
活动队列和运行队列
活动队列
过期队列
active指针和expired指针
环境变量
查看环境变量
相关指令
环境变量的组织方式
代码获取环境变量
程序地址空间
写时拷贝
mm_struct
为什么要有虚拟地址空间?
进程终止
_exit函数
exit函数
进程等待
wait
waitpid
进程程序替换
冯诺依曼体系结构
我们常见的计算机、服务器等大多数都遵守冯诺依曼体系
输入设备:键盘、鼠标、摄像头、话筒、网卡、扫描仪等
输出设备:显示器、磁盘、网卡、打印机等
中央处理器(CPU):含有运算器和控制器等
存储器:内存
关于冯诺依曼:
不考虑特殊情况;所有设备都只能和内存打交道
正是因为有了冯诺依曼体系,让当代计算机成为了性价比的产物
一般存储设备
如CPU:存储量小,访问效率快,成本高
如磁盘:存储量大,访问效率相对较慢,成本低
所以有了内存这个中存储,中速度来调节二者,从而让计算机速度更快
如果不考虑经济问题当然可以全部都用CPU级别的速度来造计算机,正是因此冯诺依曼让当代计算机成为了性价比的产物
操作系统
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)
操作系统是一个管理软硬件资源的软件
操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(函数库,shell程序等等)
设计操作系统的目的
对下:与硬件交互,管理所有的软硬件资源
对上:为软件(应用程序)提供一个良好的执行环境
- 软硬件体系结构是层状结构
- 访问操作系统必须使用系统调用(就是函数,只不过是系统提供的函数)
- 只要一个程序访问了硬件,那么它就必须贯穿整个软硬件体系结构
操作系统不相信任何用户,但我们之所以使用它是因为它会暴露自己的部分接口(系统调用),供上层开发者使用
操作系统的管理
先描述,再组织
例如要对硬件进行管理
我们可以先对硬件进行描述
用一个结构体struct将各个信息进行描述到结构体中
再组织:
用我们学习过的数据结构将n个结构体变量组织起来
具体选择哪个数据结构需要根据实际情况考虑,查找可以使用哈希表,快速插入删除可以使用链表
进程
概念:程序的一个执行实例,正在执行的程序等
内核: 担当分配系统资源(CPU时间,内存)的实体
PCB
进程信息被放在一个进程控制块的数据结构中,是进程属性的结合,我们称之为PCB,Linux操作系统下的PCB是:task_struct
Linux中进程控制块PCB-------task_struct结构体结构 - 童嫣 - 博客园
在Linux中描述进程的结构体叫做task_struct
task_struct是Linux内核的一种数据结构,它会被装在到RAM(内存)里并且包含着进程的信息
- 标⽰符: 描述本进程的唯⼀标⽰符,⽤来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执⾏的下⼀条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下⽂数据: 进程执⾏时处理器的寄存器中的数据。
- I∕O状态信息: 包括显⽰的I/O请求,分配给进程的I∕O设备和被进程使⽤的⽂件列表。
- 记账信息: 可能包括处理器时间总和,使⽤的时钟数总和,时间限制,记账号等。
- 其他信息
所有运行在系统里的进程都以task_struct链表的形式存在内核里
进程id:PID
父进程id:PPID
我们可以在代码中用getpid和getppid来查看当前进程的pid和ppid
它需要 sys/types.h 和 unistd.h两个头文件
例如:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
这里可以看出当前进程运行后它的pid为32752,ppid为32488
查看进程
进程信息可以通过/proc系统文件夹查看
这些蓝色的数字就是当前Linux系统下各个进程的pid
我们可以通过ps命令查看我们想要看的进程信息
ps ajx
作用是显示系统中所有用户的所有进程
我们可以先把头第一行的头过滤出来,然后通过grep过滤专门来看我们想要查看的进程信息
// filename为文件名
ps ajx | head -1 && ps axj | grep filename
// pid为进程id
ps ajx | head -1 && ps axj | grep pid
我们也可以循环查看
// filename为文件名
while :; do ps ajx | head -1 && ps axj | grep filename; sleep 1; done
// pid为进程id
while :; do ps ajx | head -1 && ps axj | grep pid; sleep 1; done
fork
fork是一个系统调用
它的作用是创建一个子进程
fork有两个返回值
首先fork是有返回值的
在fork函数内部return之前就已经完成了子进程的创建,子进程会接着fork接下来的语句和父进程一起执行,所以就出现了一句代码有两个返回值的情况
如果是父进程,则返回值为子进程的pid
如果是子进程,则返回值为0
若fork出错,则返回负数
父子进程代码共享,数据各自开辟一份,私有一份(采用写时拷贝)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("hello proc : %d!, ret: %d\n", getpid(), ret);
sleep(1);
return 0;
}
这里的printf语句执行了两次,足以证明有两个进程同时执行了这个printf语句
fork之后我们可以利用返回值进行用if语句进行分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0) // 出错
{
perror("fork");
return 1;
}
else if(ret == 0) //child
{
printf("I am child : %d!, ret: %d\n", getpid(), ret);
}
else //father
{
printf("I am father : %d!, ret: %d\n", getpid(), ret);
}
return 0;
}
进程状态
一个进程在运行时可能会有多种状态
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):意味着进程在等待事件完成(也叫做可中断睡眠状态)
D磁盘休眠状态(Disk sleep):也叫做不可中断睡眠状态,在这个状态的进程通常会等待IO的结束
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止T状态下的进程。这个进程可以通过发送SIGCONT信号让进程继续运行
X死亡状态(dead):这个状态只是一个返回状态,不会在任务列表中看到这个状态
Z僵死状态(Zombies):当进程退出并父进程没有读取到子进程退出的返回代码就会产生僵死进程
进程状态查看
ps aux / ps axj 命令
- a:显⽰⼀个终端所有的进程,包括其他用户的进程。
- x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
- j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
- u:以用户为中⼼的格式显⽰进程信息,提供进程的详细信息,如用户、CPU和内存使⽤情况等
僵尸进程
只要子进程还在退出,父进程还在运行,并且父进程也没有读取子进程的状态,则子进程进入Z状态,成为僵尸进程
僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
如果父进程一直不回收子进程,就会造成内存资源的浪费,因为它的PCB资源需要一直维护,C中定义一个结构体变量也是需要占用内存的
孤儿进程
如果父进程先退出,那么这个子进程就称为孤儿进程
孤儿进程会被1号init进程领养,被1号进程回收
进程优先级
cpu资源分配的先后顺序,就是指进程的优先权
ps -l
使用该命令可以查看系统进程信息
UID:代表执行者的身份
PID:代表进程的代号
PPID:代表父进程的代号
PRI:代表这个进程可被执行的优先级,值越小越早被执行
NI:代表这个进程的nice值
nice值表示进程可被执行的优先级的修正数值
PRI(new) = PRI(old) + nice
当这个PRI(new)越小就会越早被执行
所以,调整进程优先级,就是调整进程的nice值
nice值的取值范围是-20 ~ 19,一共40个级别
查看、修改进程优先级命令
先用top可以查看各进程的优先级
进入top后按 "r" ,输入进程pid,输入nice值即可完成修改优先级
也可以使用nice,renice命令、系统调用调整优先级
竞争、独立、并行、并发
进程切换
一个进程一旦占有了CPU,它的运行时间是有限的,这个时间可以叫做时间片
时间片:当代计算机都是分时操作系统,没有进程都有它合适的时间片(其实就是⼀个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来。
当进程切换的时候,操作系统会保存进程的上下文数据,当下一次轮到该进程运行时再将该数据恢复
保存到了task_struct里
活动队列和运行队列
活动队列
时间片还没有结束的所有进程都按照优先级放在活动队列
nr_active:总共有多少个运行状态的进程
queue[140]:一个元素就是一个进程队列,下标就是优先级,相同优先级按照FIFO规则进行排队调度
bitmap[5]:为了提高查找非空队列的效率,用5*32个比特位表示队列是否为空,5*32 > 140可以表示每一个下标是否为空
过期队列
过期队列和活动队列的结构一模一样
过期队列上放置的进程都是时间片耗尽的进程
当活动队列上的进程都被处理完毕后,对过期队列的进程进行时间片重新计算(过期队列变活动队列,活动队列变过期队列)
active指针和expired指针
active指针永远指向活动队列,expired指针永远指向过期队列
所以当活动队列的进程都被处理完毕后, 过期队列变活动队列,活动队列变过期队列的本质就是交换active和expired指针
环境变量
环境变量一般指在操作系统中用来指定操作系统运行环境的一些参数
我们在编写代码的时候,链接时,从来都不知道我们所链接的动静态库在哪,但是照样可以链接成功,原因就是因为有相关环境变量帮助编译器进行查找
查看环境变量
env查看全部环境变量
echo $NAME(NAME是指定环境变量的名称)
相关指令
echo $NAME:显示某个环境变量值
export:设置一个新的环境变量
env:显示所有环境变量
unset:清楚环境变量
set:显示本地定义的shell变量和环境变量
环境变量的组织方式
每个程序都会收到一张环境变量表,环境表是一个字符指针数组,每个指针指向一个以'\0'为结尾的环境字符串
代码获取环境变量
1. 命令行第三个参数
#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;
}
2. 通过第三方变量environ获取
#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;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明
3. 通过系统调用获取环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
程序地址空间
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
g_val = 100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
当我们执行上面的代码时,我们会存在一对父子进程
但是我们在子进程将g_val的值改成100时,父进程的g_val没有任何变化,说明了进程的独立性
但是它们的g_val的地址却一样的?地址一样值却不相同,能说明:
- 变量内容不一样,父子进程输出的变量绝对不是同一个变量
- 地址值一样,说明绝对不是物理地址
我们在用C/C++语言所看到的地址,全部都是虚拟地址,物理地址用户都看不到,由操作系统OS同一管理
OS必须负责将虚拟地址转化为物理地址
每个进程都有自己独立的虚拟地址空间
操作系统会通过物理内存对虚拟地址空间在页表中进行映射关系,这样就能通过页表找到该存放的物理内存
所以即使两个程序的虚拟地址相同,但通过页表映射后它们对应的物理内存空间的地址是不一样的,所以就会出现虚拟地址相同但值不同的情况
写时拷贝
父子进程的代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本
因为有写时拷贝技术的存在,所以父子进程得以彻底分离,保证了进程的独立性
mm_struct
描述Linux下进程的地址空间的所有信息的结构体都是mm_struct(内存描述符)
mm_struct结构是对整个用户空间的描述,每一个进程都会有自己独立的mm_struct,这样每一个进程都会有自己独立的地址空间能够互不干扰
为什么要有虚拟地址空间?
如果没有虚拟地址空间,那进程就是直接在物理内存中操作
1. 安全风险
每个进程都可以访问任意的内存空间,这也就意味着任意一个进程都能够读写系统相关内存区域,如果是一个木马病毒,就能直接让设备瘫痪
2. 地址不确定
当运行时直接使用物理地址,我们无法确定内存现在使用到了哪里,也就是说拷贝的实际内存地址每一次运行都是不确定的
3. 效率低下
如果直接使用物理地址,出现物理内存不够用的情况,我们一般是将不常用的进程拷贝到磁盘的交换分区中腾出内存,如果是物理地址的话就需要整个进程一起拷走这样时长太高
进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码
进程退出我们可以通过 echo $? 查看进程退出码
_exit函数
#include <unistd.h>
void _exit(int status);
status定义了进程的终止状态,父进程通过wait来获取该值
_exit是系统调用,它刷新的是系统级缓冲区
exit函数
#include <unistd.h>
void exit(int status);
exit最后也会调用_exit函数,但在调用_exit之前,还做了其他工作
关闭所有打开的流,所有在应用层级的缓存数据均被写入
最后调用_exit函数
进程等待
wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
成功则返回被等待进程的pid,失败则返回-1
参数是个输出型参数,可以获取子进程的退出状态,不关心可以设置为NULL
调用wait的父进程会随机等待任意一个子进程
waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
成功则返回被等待进程的pid,失败则返回-1,如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
参数:
pid表示需要等待的子进程,若为-1则表示等待任意一个子进程,与wait等效
status输出型参数,WIFEXITED:若为正常终止子进程返回的状态,则为真(查看进程是否正常退出),WEXITSTATUS:若WIFEXITED非零,提取子进程退出码(查看进程的退出码)
options:默认为0,表示阻塞等待。WNOHANG:若pid指定的子进程没有结束,则waitpid函数返回0,不予等待。若正常结束,则返回该子进程的ID
status参数不能简单的当作整形来看待,可以当作位图来看待
若正常退出,低7位比特位为0,8-15表示退出状态
若异常退出,低7位表示终止进程的信号,这时候的退出码则毫无意义
进程程序替换
fork之后,父子进程会执行同一个程序,但是我们也可以通过程序替换让子进程执行其他程序的代码
子进程往往要调用一种exec函数以执行另一个程序,当用户执行一种exec函数时,该进程的用户空间代码和数据会被新进程替换,从新进程启动例程开始执行
调用exec并不创建新进程,所以进程id不会改变
#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, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
返回值:
调用出错则返回-1,成功直接执行新代码,没有成功返回值
这些函数非常多,但是只要掌握了命名风格就很容易记住了,除了exec之外,还有l、p、e、v,分别具有不同含义
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path):有p自动搜索环境变量PATH
- e(env):表示自己维护环境变量
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要⾃⼰组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使⽤环境变量PATH,⽆需写全路径
execvp("ps", argv);
// 带e的,需要⾃⼰组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
这些函数中,只有execve是系统调用,其它的五个函数最终都是调用的execve
完