《操作系统导论》-1.1-虚拟化-进程概念及API
虚拟化
操作系统的基本抽象——进程。
人们希望同时运行多个程序,但CPU核心往往是屈指可数的。为了使得每个程序都有自己的CPU可用(至少看起来是这样的),系统将CPU虚拟化,让一个进程只运行一个“时间片”,然后切换到其他进程——即时分共享CPU计数
但是很明显进程共享CPU,进程间的切换会有额外的性能消耗
与“时分共享”相对的,磁盘空间就是一个“空分共享”资源
- 低级机制:实现所需功能的方法或协议,“如何实现?”
- 高级智能:操作系统内做出某种决定的算法,“更明智的决策”
进程
进程是操作系统为正在运行的程序提供的抽象
包括了但不限于诸如:
内存、寄存器、IO信息(当前打开的文件列表)
创建
程序如何被转化为一个进程?
-
首先,要把代码和所有的静态数据从磁盘加载到内存(进程的地址空间)中
现代操作系统一般惰性加载(分页和交换机制)
-
为程序运行时栈、堆分配一些内存(也可能是程序自己申请并初始化的:
malloc()
) -
IO相关的初始化操作
比如UNIX系统中的每个进程默认有三个文件描述符:标准输入、输出和错误
-
最后,通过跳转到
main()
例程,OS将CPU的控制权转移到新创建的进程中,从而程序开始执行
状态
- 就绪
- 运行
- 阻塞
只有“就绪”和“运行”可以相互切换,进程的切换需要额外的数据结构保存其上下文信息(进程列表、PCB)
API
fork()
// 这是关于调用系统API创建新进程的简单例子 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("hello world (pid:%d)\n", (int)getpid()); int rc = fork(); if (rc < 0) { fprintf(stderr, "fork failed\n"); exit(1); } else if (rc == 0) { printf("hello,I am child (pid:%d)\n", (int)getpid()); } else { printf("hello,I am parent of %d (pid:%d)\n", rc, (int)getpid()); } return 0; }
注意这个程序要在Linux(Unix)环境下运行,不然
<unistd.h>
头文件要报错,因为windows下不提供这个
这边运行的输出长这样:
hello world (pid:3125265) hello,I am parent of 3125266 (pid:3125265) hello,I am child (pid:3125266)
程序的主要内容就是在主程序中fork()
了一个子进程
注意fork()
之后两个进程是同时运行的,并不存在绝对的顺序(由CPU调度程序决定)
所以可能是parent语句先输出,也可能child语句先输出
另外,新创建的子进程虽然是和父进程完全一样的,但它并不从main入口开始执行,而是从fork()
位置开始,这也是为什么没有再输出一遍hello world
的原因
还有就是,父进程和子进程获取到返回值是不一样的,父进程获取到的是子进程的PID,而子进程获取到的fork()
返回值是0,这也是为什么两个进程运行了不同的分支的原因
wait()
父进程需要等待子进程完成的情况
// 这是关于调用系统API让父进程等待子进程执行完毕的简单例子 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { printf("hello world (pid:%d)\n", (int)getpid()); int rc = fork(); if (rc < 0) { fprintf(stderr, "fork failed\n"); exit(1); } else if (rc == 0) { printf("hello,I am child (pid:%d)\n", (int)getpid()); } else { int wc = wait(NULL);// 这里等待子进程执行完成 printf("hello,I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int)getpid()); } return 0; }
# 程序输出 hello world (pid:3129576) hello,I am child (pid:3129577) hello,I am parent of 3129577 (wc:3129577) (pid:3129576)
exec()
fork()
只能运行与父进程相同的拷贝进程,而exec()
用来运行不同的进程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main() { printf("hello world (pid:%d)\n", (int)getpid()); int rc = fork(); if (rc < 0) { fprintf(stderr, "fork failed\n"); exit(1); } else if (rc == 0) { printf("hello,I am child (pid:%d)\n", (int)getpid()); char *myargs[3]; myargs[0] = strdup("wc"); myargs[1] = strdup("testExec.c"); myargs[2] = NULL; execvp(myargs[0], myargs); printf("this shouldn't print out"); } else { int wc = wait(NULL); printf("hello,I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int)getpid()); } return 0; }
# 程序输出 hello world (pid:3131031) hello,I am child (pid:3131032) 33 80 730 testExec.c hello,I am parent of 3131032 (wc:3131032) (pid:3131031)
让我们看看发生了什么:
首先主程序运行,然后创建了一个子进程并等待子进程执行完毕
在子进程中调用了execvp()
来运行字符计数程序wc(基于本文件,打印出有多少行、多少单词、字节)
exec()
有诸多变体
很奇怪,exec()
并没有创建新进程,而是直接将当前运行的程序替换为不同的运行程序
“对
exec()
的成功调用永远不会返回”?什么意思
为什么这么设计?把fork()
和exec()
分开?
给了shell在
fork()
之后,exec()
之前运行代码的机会,在运行新程序前改变环境,比如:输出重定向,以下是个示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <sys/wait.h> int main() { int rc = fork(); if (rc < 0) { fprintf(stderr, "fork failed\n"); exit(1); } else if (rc == 0) { // 重定向标准输出到指定文件 close(STDOUT_FILENO); open("./test4.output", O_CREAT | O_WRONLY | O_TRUNC | S_IRWXU); char *myargs[3]; myargs[0] = strdup("wc"); myargs[1] = strdup("test4.c"); myargs[2] = NULL; execvp(myargs[0], myargs); } else { int wc = wait(NULL); } return 0; }
# 输出到test4.output文件中 cat test4.output 32 68 604 test4.c
机制:受限直接执行
前面我们说了,CPU的虚拟化可以通过时间片轮转的而方式,但是这样的做法同样会带来一些挑战:
-
首先是进程切换的性能开销,不可避免,但是必须控制在尽量小的范围
-
控制权,操作系统不能将CPU等系统资源的控制权完全放出
防止可能的程序恶意行为,以及需要保留能够对运行中的程序进行调度的能力
即:高效、可控
本文作者:YaosGHC
本文链接:https://www.cnblogs.com/yaocy/p/17007751.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步