1、计算机的基本组成原理
冯*诺依曼体系:
控制器 计算器 存储器 输出设备 输入设备
CPU 内存 I/O
存储设备:磁盘、硬盘 ,内存 ,高级缓存 , 寄存器
总线:所谓总线(Bus),一般指通过分时复用的方式,将信息以一个或多个源部件传送到一个或多个目的部件的一组传输线。是电脑中传输数据的公共通道。
地址总线:CPU的寻址能力
数据总线:数据传输
控制总线:传送控制信号和时序信号
CPU的位数:ALU的宽度,CPU的计算能力,一次计算能计算的数据宽度;
程序加载:
程序:磁盘上存储的二进制可执行文件;
加载:通过I/0操作将程序读取到内存上;进程文档(指令+数据);
CPU调用:在内存读取执行和数据进行计算;整个计算流程称之为进程;
2、进程管理
进程的概念:运行(加载到内存)中的程序;一组有序指令+数据+资源的集合;
系统生的进程:进程主体+进程控制块(PCB);
系统进程管理 – PCB
操作系统为了管理进程,因而通过一个 task_struct 结构体记录进程的信息
(进程标识符、优先级、进程状态、程序计数器、程序上下文、信号、打开的文件等等);
每一个进程都有一个 task_struct 结构体变量,称之为 PCB;
32位系统上,每个 PCB 大约 1.7K;操作系统通过一个双向循环链表管理所有的PCB。
进程运行状态
就绪:等待系统调度执行的进程;
运行:在 CPU 上执行的进程;
阻塞:进程被挂起,直到等待的条件发生,才将进程放到就绪队列中;
挂起:只要不唤醒,则永远不可能被执行;
僵死进程:父进程未结束,子进程结束 并且父进程未获取子进程的退出状态;
这种进程,进程主体空间已经释放,只有PCB还未释放;
解决僵死进程方法:
1)父进程调用 wait 或者 waitpid 获取子进程的退出状态,这种方式
可能导致父进程在 wait 或 waitpid 调用时阻塞运行,直到子进程退出;
2)父进程调用 singal(SIGCHLD,SIG_IGN),来忽略 SIGCHLD 信号,
这样子进程结束后会由内核释放资源;
3)对子进程的退出捕获他们的退出信号 SIGCHLD ,父进程退出信号时,
在信号处理函数中调用 wait 或 waitpid 操作来释放他们的资源;
孤儿进程:父进程结束,子进程未结束,子进程就是孤儿进程;
孤儿进程会被系统守护进程 init(PID==1)收养,并为他们完成状态搜集工作;
守护进程:又称精灵进程,常常在系统启动时自启,仅在系统关闭时才终止,
生存期比较长,一般都是在后台运行;
可通过 ps -axj 命令查看常用系统守护进程,其中最为常见的 init进程,负责各运行层次间的系统服务;
守护进程编程规则:
1)首先调用 umask(mode_t umask()) 函数将文件模式创建屏蔽字设置为0;
2)调用 fork() ,然后使父进程退出(exit);
3) 调用 setsid() 创建一个新对话;
4)将当前目录更改为 根目录;
5)关闭不再需要的文件描述符;
6)某些守护进程打开 /dev/null 使其具有文件描述符 0、1、2,
这样任何一个进程就不会产生其他不好的效果;
3、简单 “分页” 下的进程加载过程
1.系统对内存按照固定大小的 “页帧” 来管理,并且每个页帧都会有其编号(页号)-->固定分区;
2.操作系统为每一个加载内存上的进程维护一个页表 --> 进程不需要连续的存储在内存上;
3.不需要运行,程序的变量就可以指定一个地址(逻辑地址),
当通过逻辑地址访问物理地址时,需要根据页表进行转化;
4、主函数参数和缓冲区
主函数参数:int main(int argc, char* argv[], char* envp[]);
参数个数 参数列表 环境变量
主函数默认接收一个参数,就是执行的命令
缓冲区:printf: 将程序中数据写入到内存缓冲区,遇到四种情况会输出到屏幕上:
1、遇到 "\n" 2、程序结束(exit())
3、缓冲区满 (1024个字节) 4、主动刷新 fflush(stdout);
5、Linux 文件操作函数
1.C语言的库函数:fopen fread fwrite fclose fseek fgets fputs fgetc fputc
2.Linux的系统调用函数:open read write close lseek stat lstat fstat
FILE * fopen(const char *filename, const char *flag);
// "r" "w" "a" "b" "+'
int open(char *path, int flag, /* mode_t mode */ );
//打开一个普通文件,如果打开成功,返回文件描述符;
int fread(void *buff, int size, int count, FILE *fp);
int read(int fd, void *buff, size_t size);
//size:指定一次最多读取的字节长度,一般都是 buff 中实际的数据长度
//按字节读取文件内容;
int fwrite(void *buff, int size, int count, FILE *fp);
int write(int fd, void *buff, size_t size);
//按字节给文件写入内容;
int fclose(FILE *fp);
int close(int fd);
//关闭打开的文件;
int fseek(FILE *fp, int size, int pos);
int lseek(int fd, int size, int pos);
//移动文件读写偏移量;
三个 stat 函数获取文件的属性信息:
int stat(char *path, struct stat *st);
int fstat(int fd, struct stat *st);
int lstat(char *path, struct stat *st);
扫描目录:
opendir readdir telldir closedir seekdir
-
库函数和系统调用函数的区别:
库函数:在函数库文件中,调用是在用户态,执行也在用户态; 库函数有可能还需要转调系统调用函数,比如:fopen、printf等; 也有可能不需要转调系统调用函数,比如:strlen、strcpy等; 系统调用函数:系统内核提供给上层访问(用户空间调用)的接口, 在系统内核中实现;所以其调用是在用户态,而执行是在内核态;
-
系统调用函数的实现原理(用户态切换内核态的流程)
系统调用函数触发 0x80 中断,并且将系统调用号存储在eax寄存器中, 然后陷入内核,内核开始执行中断处理程序,在系统调用表中查找 系统调用号对应的系统内核函数并调用,执行完成后又将返回值通过 eax 寄存器传递回用户空间; (0x80 中断:中断处理程序,内核态执行) (系统调用表:call[sys_call_table + 4*eax])
-
进程在 PCB 中记录打开文件资源的结构
PCB: struct file *filp[20]; return fd; -->filp数组的下标; struct file: *m_inode f_pos f_count; struct m_inode: 文件的属性信息;
6、多进程编程 --> 进程创建
-
fork 函数
函数原型:pid_t fork(void); 函数返回类型 pid_t 实质是 int 类型 fork函数会新生成一个进程,调用fork函数的进程为父进程,新生成的进程为子进程 特点: 1.fork 函数调用一次,返回两次,在父进程中返回子进程的pid,在子进程中返回0; 2.子进程从fork之后开始执行,并且fork之后的代码父子进程都会执行, 只有子进程执行的代码必须放到if(n==0)块中,父进程执行的放到else代码块中; 3.fork生成的子进程后,子进程和父进程谁先运行是不确定的,fork函数执行完成, 则子进程和父进程都是独立运行的; 父进程处理子进程的退出状态:pid_t wait(int *reval); pid_t waitpid(pid_t pid, int *reval, int flag); // 非阻塞
-
父子进程的数据共享 --> 写时拷贝技术
全局数据(.data .bss)、堆数据(.stack)、栈区数据(.heap malloc/new) -->进程都是不共享的; --> 进程的4G的虚拟地址空间+每个进程都会有自己的页表; 文件描述符:有可能共享(fork之前打开的文件描述符); 子进程的PCB是复制父进程的PCB(浅拷贝); 写时拷贝技术:fork之后,刚开始父子进程共享数据+指令段,内核将其共享的空间设置为只读的, 如果任意一个进程试图修改共享空间的数据,则将修改的数据所在的 "页" 拷贝一份; 作用 :1.这样可以延迟页面拷贝,提高 fork 复制的效率; 2.通常 Linux 中的新进程都是通过fork+exec 实现的, 如果 fork 后需要执行exec 那么直接就不用拷贝了。
-
vfork 函数
vfork函数原型:pid_t vfork(void); 特点:父子进程共享数据段,并保证子进程先于父进程运行, 在它调用 exec 或 _exit 时,父进程才会被运行;
-
进程切换(schedule)
进程切换指从正在运行的进程中收回处理器,让待运行进程来占有处理器运行。 实质上就是被中断运行进程与待运行进程的上下文切换。 进程切换一定发生在中断/异常/系统调用处理过程中,常见的有以下情况: 1、阻塞式系统调用、虚拟地址异常;导致被中断进程进入等待态。 2、时间片中断、I/O中断后发现更改优先级进程;导致被中断进程进入就绪态。 3、终止用系统调用、不能继续执行的异常;导致被中断进程进入终止态。
7、 信号
1.信号的概念:
信号是系统预先定义好的特定的一些事件,信号可以被产生,也可以被接收,
产生和接收的主体都是进程;
2.信号的响应方式 :
默认 忽略 自定义 (捕获)
SIG_IGN SIG_DFL 用户函数 void signal_fun(int sign);
修改信号的响应方式:
signal(int sigtype, void(*sig_handler)(int));
函数指针,给定用户自定义的函数
3.发送信号:
int kill(pid_t pid, int sigtype);
8、进程替换 exec
用exec 函数的第一个参数指定的程序替换调用exec函数的进程的进程主体部分;
execl execv execle execve execlp execvp
int execv(char *filename, char *argv[]);
int execve(char *filename, char *argv[], char *envp[]); //系统调用函数
exec函数:如果调用成功,则返回值无意义;
如果调用失败,则返回-1;
10、 进程间通讯
五种方式比较
内容 效率 共享实质 进程数量 阻塞机制
有名管道 数据 低 磁盘上文件标识 两个 read
无名管道 数据 低 父子进程共享文件描述符 父子两个 read
消息队列 消息 低 内核对象 n msgrcv
信号量 同步 - 内核对象 n P操作
共享内存 数据 高 内核对象 n P操作
-
管道 —半双工通讯
有名管道:在磁盘上管道文件标识存在,但是使用时,其并不占磁盘空间,数据缓存在内存上; 无名管道:借助于父子进程间共享fork之前打开的文件描述符,数据缓存也是在内存上; 通过无名管道完成通讯,不会创建管道文件,只能应用于父子进程之间; 全双工通信 与 半双工通信 半双工:数据可以从 A 到 B 发送,也可以从 B 到 A 发送,但某一时刻,只能是一个方向; 全双工: 数据在任一时刻,可以在两个方向同时进行,即 A 到 B 和 B 到 A; 创建管道文件: 命令:mkfifo filename 函数:int mkfifo(char *filename, int mode); 打开:open 会阻塞运行,至少有一个读进程,一个写进程; 写:write 阻塞运行,管道空间满; char buff[128]; fgets(); write(fd, buff, strlen(buff)); 读:read 会阻塞,有数据可读 或者 所有写端关闭; 关闭:close 创建并打开无名管道: int pipe(int fds[2]); fds[0] 读文件描述符 fds[1] 写文件描述符
-
信号量 ----- 控制进程同步的一种机制
信号量: 类似于一个计数器,当信号量的值大于0时,其值表示临界资源的数量, 当其值等于0时,表示当前无临界资源可用,所有P操作都将被阻塞, 直到有进程释放资源,执行V操作,将信号量的值+1; ipcs -s 查看信号量 ipcrm -s id 删除信号量 当进程访问临界资源前:对信号量进行 P操作 当进程访问临界资源后:对信号量进行 V操作 必须都是原子操作 信号量的内核对象 --> 使多个进程能够访问到同一个信号量 内核维护的内核对象记录的是一个 信号量集 单方面:A进程给B进程提供服务 进程对普通文件的 read 是非阻塞的,A进程将数据写入文件中, B进程从文件中读取写入的数据,并将其打印; A进程:不需要阻塞,信号量初始值:0; B进程:raed之前必须阻塞,等待A进程讲数,据写入文件; A进程和B进程交替打印字符串 A父进程 B子进程 1.临界资源:同一时刻只允许一个进程(线程)访问的资源; 2.临界区:访问临界资源的代码段叫临界区; 3.原子操作:不能被打断的操作; 4.操作系统中的P、V操作 1、P V 操作 都是对信号量而言的; 2、P 操作 是对信号量的值进行原子减一,代表获取资源,当信号量的值为0时, P操作会阻塞,意味着资源不可用; 3、V 操作 是对信号量的值进行原子加一,代表释放资源,V 操作从不阻塞。 5.阻塞、非阻塞 1、阻塞和非阻塞关注的是进程在调用函数时的状态; 2、阻塞:进程被挂起,死等条件的发生; 3、非阻塞:立即返回,继续执行下面的指令; 6.同步、异步 1、同步和异步关注的是消息通信机制; 2、同步:没有通知机制,只能一直探测条件是否发生,实现多进程协同工作; 3、异步:进程发出请求后,不需要等待,直接处理后续工作,当条件发生后, 通知内核的消息通知机制,通知进程处理之前的请求; 7.同步和互斥 1、互斥:某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性; 但互斥无法限制访问者对资源的访问顺序,即访问是无序的; 2、同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问; 8.串行、并发、并行 串行:按顺序执行任务: 多个任务,执行时一个执行完再执行另一个; 并发:多个线程在单个核心运行,同一时间一个线程运行,系统不停切换线程, 看起来像同时运行,实际上是线程不停切换; ( 在单CPU系统中,系统调度在某一时刻只能让一个线程运行, 虽然这种调试机制有多种形式(大多数是时间片轮巡为主),但无论如何, 要通过不断切换需要运行的线程让其运行的方式就叫并发; ) 并行:每个线程分配给独立的核心,线程同时运行; ( 而在多CPU系统中,可以让两个以上的线程同时运行, 这种可以同时让两个以上线程同时运行的方式叫做并行(parallel); ) 信号量的操作: 创建或获取信号量: int semget((key_t)key, int nsems, int flag); nsems: 信号量集中信号的个数; 如果是新创建的信号量,必须做初始化设置: int semct(int semid, int semnum, int cmd, union semun arg); semnum: 操作信号量集中的那一个信号量 cmd = SETVAL arg.val = 信号量的初始值 如果是获取的,则可以直接使用: id = semget(); //仅仅获取信号量 if(id == -1) { id = semget(); //创建 //初始化 } int semop(int semid, struct sembuf buf[], size_t size); struct sembuf { short sem_num; //指定本变量操作的信号量集中信号量的下标 short sem_op; // -1 P操作 1 V操作 short sem_flg; // SEM_UNDO } 释放内核对象: int semct(int semid, int semnum, int cmd); //立即删除 // cmd = IPC_RMID 获取:int SemGet(int key, int val[], int nsems); P操作:int SemWait(int semid, int sems[], int len); V操作:int SemPost(int semid, int sems[], int len); 释放:int SemDel(int semid);
-
消息队列 ---- 数据的定向发送与接收
消息:类型(标识符)+数据 一条消息 发送的消息都是独立的 队列:优先级(类型)队列 -->在类型确定下,遵循先进先出的原则; 1、进程读取消息队列时,指定读取的消息类型,则可以实现定向发送消息; 2、消息不是字节流的数据,是按照条来计算消息的,所以一次读取,只能读取一条消息中的数据; ipcs -q 查看消息队列 ipcrm - q id 删除消息队列 用到的函数: int msgget((key_t)key, int flag); key值:用户标识符; flag: 权值 IPC_CREAT 返回值:失败返回-1, 成功返回内核对象的内核ID int msgsnd(int msgid, void *ptr, size_t nbytes, int flag); btytse: 数据部分的实际长度; ptr 指向的结构: struct msgdata { long mtype; 类型 char mtext[128]; 数据 } int msgrcv(int msgid, void *ptr, int nbytes, long type, int flag); ptr: 指向 msgdata 结构,用于保存接收到的消息; nbytes: 指定接收数据的缓冲区的大小 type:本次接收数据的类型 int msgctl(int msgid, int cmd, struct msgid_ds *buf); cmd: IPC_S-TAT IPC_SET IPC_RMID
-
共享内存
共享内存: 通过共享内存的内核对象,将两个需要通讯的进程的两个虚拟地址映射到相同的物理内存空间上, 从而使得进程能够访问相同的物理内存空间,来实现数据的共享; 但是,共享内存对于多进程就是一种临界资源; (是最快的一种IPC:共享内存在使用时,会比管道少两次数据的拷贝) (多进程使用共享内存空间时就必须做到同步控制--信号量) (系统上的进程,每个进程都有4G虚拟地址空间,而进程的真是的物理空间是独立的; 在物理内存上找一块空间,使得多个进程都能访问这块空间) 实现步骤: 1.创建内核对象,并申请物理内存; 2.各进程中,分别将自己的虚拟地址通过内核对象映射到开辟物理内存上,(链接) 3.分别访问这块内存,通过 ptra ptrb 4.断开链接,将虚拟地址与物理内存断开联系; 5、删除内核对象; ptra: 指向物理内存空间 fgets(ptra,128, stdin); 共享内存的操作: int shmget((key_t)key, int size, int flag); size: 申请的共享内存空间大小; void *shmat(int shmid, const void *attr, int flag); 返回链接的虚拟地址 失败返回-1 int shmct(int shmid, int cmd, struct shmid_ds *buf); 不会立即删除,但是其他进程就不能通过 shmat 与该段链接