计算机组成
- 电脑由一大堆硬件组成
- CPU,主板,显卡,网卡,内存,硬盘,鼠标,键盘,显示器,还有一堆线。
- CPU是计算机最核心的硬件,它分三个部分组成:
- 运算单元
- 用来完成各种算术和逻辑运算。
- 数据单元
- 运算单元需要的数据和计算的结果得有地方保存,这就是数据单元的作用,它的速度比内存还快很多,但是存储空间有限,很多数据还是要保存到内存中。
- 控制单元
- 统一的指挥中心,它指导运算单元取出数据单元的数据,计算出结果,然后放在数据单元的某个地方。
- 运算单元
- CPU中有非常多的寄存器,《趣谈Linux操作系统》中有专门的章节讨论这块,之后会有详细记录。
- 以上的硬件大致可以分为:输入设备、运算单元、控制单元、存储单元和输出设备。
- 输入设备和输出设备这个很好理解,键盘、鼠标、显示器都是常用的输入输出设备。
- 运算单元、控制单元这都是由CPU提供的。
- 此外CPU还提供了一部分存储单元的能力,但是更多的数据还是存储在内存和硬盘中。
- 设备之间需要数据传递,使用的是总线。
- 这就形成了冯诺依曼描述的大名鼎鼎的冯诺依曼体系。
操作系统都做了些什么事情
-
一堆硬件组成了计算机,但是光靠把这些东西组装起来,是无法跑起来代码的,还需要操作系统。
-
操作系统不是硬件,它是一个软件,主要的职责就是管理以上那些硬件。
-
从上帝视角去看操作系统,它做为一个硬件大统领,可以根据职责分为很多子系统,以系统调用(System Call)的形式对外提供功能。Linux命令,可视化界面以及代码程序使用硬件资源都是通过系统调用。
系统调用
fork
-
重点解释fork系统调用,因为它在之后的进程管理学习中有至关重要的地位。
-
在操作系统中,一个程序跑起来之后,就会形成一个进程。相比躺平的程序代码,进程是程序代码活着的样子。
-
fork是用来创建进程的,这和平时编程中的创建线程或者go语言中开启goroutine不一样,这是进程!
-
但它中文意思却不是”新建“,而是”分支“。
-
那是因为一个进程的结构很复杂,操作系统创建新进程不是从零做起,而是选择采用复制老进程的方式,所以才被叫做”分支“。
-
fork系统调用执行完成后,多了一个子进程,而在执行完毕后的这一个瞬间,它是完全等于它的父进程的,那么父子两个进程接下来要做的事情是完全一样的。
-
#include <unistd.h> #include <stdio.h> int main () { printf("before fork() \n"); fork(); printf("after fork() \n"); }- 如果运行以上的代码,终端会打印两次"after fork()"。因为出现了两个完全一样的进程,相当于fork()之后的代码会运行两遍(之前的不会)。
- 如果运行以上的代码,终端会打印两次"after fork()"。因为出现了两个完全一样的进程,相当于fork()之后的代码会运行两遍(之前的不会)。
-
而我们最终的目的肯定是希望父子两个进程去做不一样的事情,那怎么区分这两个进程谁是子谁是父呢?
-
fork()执行完后会有两个进程,后面的代码可以通过fork()的返回值来区分自己是子进程还是父进程,fork()返回进程id,如果它自己是子进程会返回0,如果它自己是父进程会返回子进程的进程号。
-
#include <unistd.h> #include <stdio.h> int main () { printf("before fork() \n"); pid_t fpid; fpid = fork(); if (fpid == 0) { // 子进程分支 // 做希望子进程做的事情 一般来说是去执行另一个程序代码,需要用到另一个系统调用 execve // 这里简单模拟一下 printf("i am child process \n"); }else if (fpid > 0){ // 父进程分支 接着干原本相干的事情 printf("i am parent process \n"); }else{ printf("error"); } }- 执行结果如下
- 执行结果如下
-
-
这样就fork出了一个不一样的子进程,父进程还可以通过waitpid系统调用传入子进程的id,等待子进程执行完,这样父进程就能知道子进程的情况。
系统调用总览
- 在之后的学习过程中,会接触到越来越多的系统调用。这里先放一个大概的总览。
“中介”
-
用go启动一个web服务。
-
func main() { http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) { writer.Write([]byte("go web")) }) err := http.ListenAndServe(":80", nil) if err != nil { log.Fatalf("ListenAndServe error:%+v",err) } } -
很简单的代码就启动了一个监听在80端口上的web服务。
-
这个服务至少是需要用到网络设备,而刚刚说过,对于硬件设备的使用都要用过系统调用,但是在写代码的过程中似乎没有感知到我们使用了系统调用。其实这是go的标准库帮我们封装好了,相当于代码和操作系统之间多了一层中介。
-
比如Glibc是Linux下使用的开源的标准C库,它最重要的功能是封装了系统调用。go代码编译过程中最终也会使用Glibc。
-
“中介”的存在大大提高了开发效率,但是最终还是会回到系统调用来使用计算机硬件资源。
-