OpsDev 团队从九月中旬启动了“公开课”形式的技术分享活动,由某领域擅长的同学系统性向大家介绍技术要点和细节。第一期公开课由李钢老师开讲,内容为“应用编程基础课”,分为 5 节课,每节课的课程大纲和重点干货我们都会整理成文分享出来供更多的同学一起学习。
前言
本人从事 linux 下 web 编程多年,最近有幸给组内同学做培训,希望能给大家介绍下自己这些年在应用编程方面的经验,今天先给大家介绍下一些编程方面的需要掌握的基础知识。
1 操作系统介绍
先来看一个 unix 系统的架构图:
从内向外,unix 系统架构分为:
1.内核:控制硬件资源,提供应用程序运行的环境
2.系统调用:内核的编程接口
3.shell 和库函数:为应用程序提供编程、运行接口
4.应用程序:我们自己编写的程序
2 系统调用和库函数
应用程序可以调用系统调用和库函数,很多库函数都会调用系统调用,用下面这幅图来展示二者的区别:
上面说过,内核用于控制硬件资源。例如从磁盘上读写文件,相当于需要控制硬盘这个硬件做 IO 操作,这个事情需要内核来做,那如何告诉内核要做这个事情呢?系统调用就是干这个事情的,它是内核暴露出来的一组编程接口,通过调用这个接口来执行内核中的代码。
库函数不是内核代码,是更高一层的功能封装。如我们常用的 printf 函数,我们可以调用这个函数输出内容到显示器上,但控制显示器的输出是内核做的事情,系统调用提供的是 write 方法,printf 相当于封装了 write 这个系统调用,给应用程序提供了一个更加友好的操作方式。
总结下,系统调用相当于内核代码的调用入口,库函数是对应用程序要使用的功能的一层友好的封装。
3 用户态和内核态
程序在运行时会有用户态和内核态的区别,请见下图:
简单一句话,程序执行时,如果执行的是我们编写的应用程序的代码,这些代码就是运行在用户态的;当代码中调用了系统调用后,接下来内核中的代码就会执行,内核中的代码就是运行在内核态的。
4 程序是如何执行的
我们用最经典的 hello world 来说明下程序是如何执行的。
程序源码 hello.c:
首先,我们开发的 hello.c 存储在磁盘上,我们首先编译它,得到可执行文件 hello:
这一步编译,实际上经历了如下处理流程:
当我们运行 hello 程序时,效果如下:
实际执行流程如下:
首先,我们通过键盘输入./hello,shell 读取每个字符到寄存器中,然后存储到主存中:
当我们输入 enter 后,shell 知道我们完成了命令输入,将 hello 这个程序从磁盘加载到主存:
使用 DMA(direct memory access)技术,数据从磁盘直接加载到主存中,避免了通过 CPU 传输。
当代码加载到主存后,CPU 开始执行程序指令,这些指令拷贝 hello, world\n 字符串从内存到寄存器,然后传输到显示设备,显示设备负责显示结果:
5 进程、线程、协程
我们先来看下一个经典的程序在内存中的布局:
1.text 段就是我们的程序代码
2.initialized data 中是我们代码中明确初始化的一些全局变量、静态变量
3.bss 段中放的是代码中没有初始化的全局变量、静态变量
4.heap 是我们程序中动态申请的内存空间,例如调用 malloc,这个空间很大
5.stack 中的空间由程序中的局部变脸和函数调用使用
进程
什么是进程呢?我理解进程就是运行中的程序在操作系统中的一个具象化的表现。
每个进程在内存中都拥有上面那些布局,而且都是独立的,不共享。
线程
那么什么是线程呢?我自己理解,每个线程代表了一个独立的执行流。
举个例子,hello 那个程序中,如果 printf 语句要执行 10 次,那么这 10 次可以一个挨着一个顺序来执行,这是一个执行流;
也可以创建 10 个执行流,每个都执行一次 printf,如果我们有至少 10 个 cpu 核,这样就可以做到 10 个 printf 并行执行。
所以,线程是操作系统调度的最小单元。
同一进程的多线程间,栈空间(stack)是相互独立的,而其它数据是共享的,所以高性能的多线程编程首先要解决的就是锁的问题。
协程
一些现代的编程语言会有这个概念,它又是什么呢?
我理解,协程是一个用户态的概念。
线程是由操作系统内核进行调度的,我们无法干预,协程是用户态程序,相当于应用程序自己进行了调度。
因为它是用户态程序,所以相当于多个协程会运行在一个线程中。
要注意的是,只有内核对线程的调度才能够利用 cpu 的多核资源,让程序做到并行,所以在一个线程中的多个协程,是无法做到并行的。
父子进程间的共享
我们来看一张图:
这张图描述的是当我们在程序中用 open 这个系统调用打开一个文件时,操作系统中维护的相关数据结构,这里最重要的一个说明,就是最左面 process table entry 部分可以理解为是用户态程序中的,其它的 file table entry 和 v-node table entry 是内核中的。
当我们使用 fork 创建新进程后,用户态的内容都会复制一份给子进程,而内核中的部分会保持不变,如下图:
所以,父子进程相当于可以通过相同的文件描述符来共享内核中的数据结构。这个特性,就是未来我们做服务的平滑重启的最重要的一点。
6 结束语
上面这些,都是笔者编程这些年,觉得非常受用的基础知识。正确的认识这些知识,很多问题你都可以自己想明白了。
笔者也是在不断学习中,如果有错误的地方,还望指正,我们共同进步,谢谢!
本文转载自公众号 360 云计算(ID:hulktalk)。
原文链接:
https://mp.weixin.qq.com/s/5XjlxkNwapl_F60sgDNXXw
评论