(一)系统调用

389 阅读5分钟

计算机组成

  • 电脑由一大堆硬件组成
    • CPU,主板,显卡,网卡,内存,硬盘,鼠标,键盘,显示器,还有一堆线。
    • CPU是计算机最核心的硬件,它分三个部分组成:
      • 运算单元
        • 用来完成各种算术和逻辑运算。
      • 数据单元
        • 运算单元需要的数据和计算的结果得有地方保存,这就是数据单元的作用,它的速度比内存还快很多,但是存储空间有限,很多数据还是要保存到内存中。
      • 控制单元
        • 统一的指挥中心,它指导运算单元取出数据单元的数据,计算出结果,然后放在数据单元的某个地方。
    • CPU中有非常多的寄存器,《趣谈Linux操作系统》中有专门的章节讨论这块,之后会有详细记录。
    • 以上的硬件大致可以分为:输入设备、运算单元、控制单元、存储单元和输出设备。
      • 输入设备和输出设备这个很好理解,键盘、鼠标、显示器都是常用的输入输出设备。
      • 运算单元、控制单元这都是由CPU提供的。
      • 此外CPU还提供了一部分存储单元的能力,但是更多的数据还是存储在内存和硬盘中。
      • 设备之间需要数据传递,使用的是总线
      • 这就形成了冯诺依曼描述的大名鼎鼎的冯诺依曼体系。
        • 冯诺依曼体系.png

操作系统都做了些什么事情

  • 一堆硬件组成了计算机,但是光靠把这些东西组装起来,是无法跑起来代码的,还需要操作系统。

  • 操作系统不是硬件,它是一个软件,主要的职责就是管理以上那些硬件。

  • 从上帝视角去看操作系统,它做为一个硬件大统领,可以根据职责分为很多子系统,以系统调用(System Call)的形式对外提供功能。Linux命令,可视化界面以及代码程序使用硬件资源都是通过系统调用。

    系统子调用总览.png

系统调用

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()之后的代码会运行两遍(之前的不会)。 fork_test1.png
    • 而我们最终的目的肯定是希望父子两个进程去做不一样的事情,那怎么区分这两个进程谁是子谁是父呢?

    • 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_test2.png
  • 这样就fork出了一个不一样的子进程,父进程还可以通过waitpid系统调用传入子进程的id,等待子进程执行完,这样父进程就能知道子进程的情况。

系统调用总览

  • 在之后的学习过程中,会接触到越来越多的系统调用。这里先放一个大概的总览。

系统调用总览.png

“中介”

  • 用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。

    • “中介”的存在大大提高了开发效率,但是最终还是会回到系统调用来使用计算机硬件资源。