1.冯诺伊曼体系结构: 数据按照二进制存储 数据存储在存储器(内存)当中 输入设备 存储器 中央处理器 输出设备
2.操作系统: 先组织,再描述
系统调用与库函数:
系统调用
系统调用指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。它通过软中断向内核态发出一个明确的请求。系统调用实现了用户态进程和硬件设备之间的大部分接口。
库函数
库函数用于提供用户态服务。它可能调用封装了一个或几个不同的系统调用(printf调用write),也可能直接提供用户态服务(atoi不调用任何系统调用)。
常见系统调用
open, close, read, write, ioctl,fork,clone,exit,getpid,access,chdir,chmod,stat,brk,mmap等,需要包含unistd.h等头文件。
常见库函数
printf,scanf,fopen,fclose,fgetc,fgets,fprintf,fsacnf,fputc,calloc,free,malloc,realloc,strcat,strchr,strcmp,strcpy,strlen,strstr等,需要包含stdio.h,string.h,alloc.h,stdlib.h等头文件。
区别:
系统调用通常不可替换,而库函数通常可替换
系统调用通常提供最小接口,而库函数通常提供较复杂功能
系统调用运行在内核空间,而库函数运行在用户空间
内核调用都返回一个整数值,而库函数并非一定如此
POSIX 标准针对库函数而不是系统调用 库函数移植性更好
系统调用运行时间属于系统时间,库函数运行时间属于用户时间
调用系统调用开销相对库函数来说更大
3.程序 & 进程
程序是永存的;进程是暂时的,是程序在数据集上的一次执行,有创建有撤销,存在是暂时的;
程序是静态的观念,进程是动态的观念;
进程具有并发性,而程序没有;
进程是竞争计算机资源的基本单位,程序不是。
进程和程序不是一一对应的: 一个程序可对应多个进程即多个进程可执行同一程序; 一个进程可以执行一个或几个程序
4.操作系统如何管理进程
描述:struct task_struct{...} 组织:双向链表
5.struct task_struct{...}
task_struct是Linux内核的一种数据结构,它会被装载到RAM里并包含进程的信息。每个进程都把它的信息放在task_struct这个数据结构里面,而task_struct包含以下内容:
标示符:描述本进程的唯一标示符,用来区别其他进程。
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级。
程序计数器:程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据:进程执行时处理器的寄存器中的数据。
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和正在被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟总数,时间限制,记账号等。
pid:pid是程序被操作系统加载到内存成为进程后动态分配的资源,每次程序执行时,操作系统都会重新加载,pid在每次加载的时候都是不同的。pid是唯一的,一个pid只标识一个进程。
理解进程切换:进程状态 程序计数器 上下文信息
进程状态:运行状态-R:一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。浅度睡眠状态-S:一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))。深度睡眠状态-D:一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)。暂停状态-T:在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。我们对一个进程发送SIGSTOP信号,该进程就进入到了暂停状态。僵尸状态-Z:当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)。首先,僵尸状态的存在是必要的,因为进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作。进程退出的信息(例如退出码),是暂时被保存在其进程控制块当中的,在Linux操作系统中也就是保存在该进程的task_struct当中。死亡状态-X:死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。
程序计数器:程序计数器是用于存放下一条指令所在单元的地址的地方
冯 ·诺伊曼计算机体系结构的主要内容之一就是“程序预存储,计算机自动执行”!程序计数器(PC )正是起到这种作用,所以通常又称之为‘指令计数器’。CPU总是按照PC的指向对指令序列进行取指、译码和执行,也就是说,最终是PC 决定了程序运行流向。故而,程序计数器(PC )属于特别功能寄存器范畴,不能自由地用于存储其他运算数据。在CPU控制部件中的程序计数器(PC)的功能是用于存放指令的地址。程序执行时,PC的初值为程序第一条指令的地址,在顺序执行程序时,控制器首先按程序计数器所指出的指令地址从内存中取出一条指令,然后分析和执行该指令,同时将PC的值加1指向下一条要执行的指令。
上下文信息:
内存指针:指向了进程虚拟地址空间
虚拟地址空间中每个虚拟地址指向哪里有3种情况:
a.未分配,这个虚拟地址仅仅是个数字而已,没有任何指向。
b.未缓冲,这个虚拟地址指向了磁盘的某个字节存储单元,里面存储了指令或者数据。
c.已缓冲,这个虚拟地址指向了物理内存的某个字节存储单元,里面存储了指令或者数据。
代码区和数据区域:来自于可执行文件,代码区和数据区挨着,代码区总是在0x0040000地址以上,0x0040000地址以下另有它用。
运行时堆区域:它初始化大小为0,随着动态分配内存(malloc),运行时堆不断往高地址方向扩展,有个指针brk指向了堆的最高地址。
共享库的内存映射区域:这个区域是一些标准的系统库,这个共享库在物理内存中只存储一份,每个进程将这个区域的虚拟地址映射到同一份共享库物理内存上。
用户栈区域:这个区域紧挨着内核区域,处于高地址处,随着用户栈的出栈,入栈,动态扩展,入栈向低地址方向扩展,出栈则向高地址方向收缩,栈顶指针存储在栈寄存器(ESP)中。
内核区域:这个区域是操作系统自己代码,数据,栈空间,内核在物理内存中只存储一份,每个进程将这个区域的虚拟地址映射到同一份内核物理内存上。
每个虚拟页可以有三种状态,未分配,已缓冲,未缓冲
未分配:虚拟页还没有分配磁盘空间
已缓冲:虚拟页缓冲或者映射在了物理页上。
未缓冲:虚拟页分配了磁盘空间,但没有在物理页上缓冲。
通常操作系统加载可执行文件后,创建了一个进程,这个进程就有了虚拟地址空间,这并不意味着可执行文件已经从磁盘加载到内存中了,操作系统只是为了进程虚拟地址空间的每个区域分配了虚拟页。
代码和数据区域的虚拟页被分配到了可执行文件的适当位置,此时虚拟页状态为未缓冲,虚拟页指向了磁盘地址。
操作系统和共享库的虚拟页被映射到了物理内存,因为操作系统和共享库已经在物理内存了,这些虚拟页的状态为已缓冲。
用户栈,运行时堆的虚拟页没有任何分配,不占用任何空间,这些虚拟页的状态为未分配。
虚拟地址由虚拟页号+虚拟页偏移量组成,虚拟页偏移量是相对某个虚拟页的偏移量。
物理地址由物理页号+物理页偏移量组成,物理页偏移量是相对某个物理页的偏移量,
页表是建立虚拟页号和物理页号映射关系的表结构,每个页表项(PTE)包括了有效位,物理页号,磁盘地址等信息
现代的CPU和操作系统为了加快虚拟地址翻译物理地址的过程,做了以下两点优化:
1.建立了虚拟号(VPN)和页表项(PTE)的映射关系,存储在TLB中,当MMU根据虚拟地址获取页表项时,先查询TLB,在TLB找到了页表项后,就不需要从高速缓冲或者内存中获取了,找不到了才会计算页表项地址PTEA,然后再从高速缓冲或者内存中获取页表项(PTE)。
2.某些热点物理地址对应的数据,存储在L1缓冲中,MMU根据物理地址获取页表项或者代码数据时,先从L1缓冲中获取,找不到再从内存中获取。
记账信息:打开进程记账功能后,内核会在每个进程终止时将一条记账信息写入系统级的进程记账文件。这条账单记录包含了内核为该进程所维护的多种信息,包括终止状态以及进程消耗的CPU时间。借助于标准工具(sa(8) 对账单文件进行汇总,lastcomm(1)则就先前执行的命令列出相关信息)或是定制应用,可对记账文件进行分析。
6.创建子进程:
fork函数:用于创建一个进程,所创建的进程复制父进程的代码段/数据段/BSS段/堆/栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化;实际上,在fork之后我们通常使用if语句进行分流,即让父进程和子进程做不同的事。
返回值:在父进程中,fork返回新创建子进程的进程ID;在子进程中,fork返回0;如果出现错误,fork返回一个负值
fork的执行过程:申请PID 申请PCB结构 复制父进程的PCB 将子进程的运行状态设置为不可执行的 将子进程中的某些属性清零,某些保留,某些修改 复制父进程的页(用到了写时拷贝技术)
写实拷贝技术: 父子进程在初始阶段共享所有的数据(全局、 栈区、 堆区、 代码), 内核会将所有的区域设置为只读。 当父子进程中任意一个进程试图修改其中的数据时, 内核才会将要修改的数据所在的区域(页) 拷贝一份。
僵尸进程:
产生的原因:如果子进程先于父进程退出,同时父进程太忙了,无瑕回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程
模拟代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(count){
printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child quit...\n");
exit(1);
}
else if(id > 0){ //father
while(1){
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else{ //fork error
}
return 0;
}
解决僵尸进程的方案:1. 杀死父进程 2. 重启操作系统 3. 进程等待
僵尸进程的危害
僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。
孤儿进程:若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。
模拟代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(1){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0){ //father
int count = 5;
while(count){
printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father quit...\n");
exit(0);
}
else{ //fork error
}
return 0;
}
没有孤儿状态:孤儿进程不是一种状态!!!而是一种进程种类的名称。
7.环境变量
一般是指在操作系统中用来指定操作系统运行环境的一些参数,是操作系统为了满足不同的应用场景预先在系统内预先设置的一大批全局变量。
PATH LD_LIBRARY_PATH HOME
如何修改 命令范式export xxx 命令行临时修改(set命令) 文件当中永久修改( 通过修改.bashrc文件:通过修改profile文件:通过修改environment文件:)
用代码如何获取环境变量:main函数的参数 getenv environ
进程虚拟地址空间:
8.进程控制
进程创建:写时拷贝
进程终止:代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。
exit & _exit函数的区别:exit() 除了要退出程序之外,还要执行终止处理程序以及标准IO清理、关闭操作(该输出的输出、该写入文件的写入文件),_exit() 和只执行程序退出操作。只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。
9.进程等待
子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
wait:阻塞等待 pid_t wait(int* status);返回值:等待成功返回被等待进程的pid,等待失败返回-1。参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。作用:等待任意子进程。退出信号 coredump标志位 退出码
waitpid:可以设置为非阻塞属性 函数原型:pid_t waitpid(pid_t pid, int* status, int options); 参数:pid:待等待子进程的pid,若设置为-1,则等待任意子进程。status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。options:当设置为WNOHANG时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。
作用:等待指定子进程或任意子进程。返回值:等待成功返回被等待进程的pid。如果设置了选项WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0。如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
进程程序替换:替换的原理;替换进程的代码段和数据段, 更新堆栈
exec函数;