- 1 如何查看一个进程内存使用的情况?
- 2 Linux的一些其他问题
- 3 进程线程以及协程的区别(拥有资源,系统开销,应用场景,相互关系)?
- 4 Linux进程之间通信的6种方式(InterProcess Communication,IPC)
- 5 内存管理
- 6 中断与异常与软中断?
- 7 Linux的文件描述符
- 8 Linux的虚拟文件系统(一切皆文件)
- 9 Linux的用户/内核态,系统调用原理
- 10 Linux IO模式及 select、poll、epoll详解
- 11 僵尸进程,孤儿进程?
- 12 Linux进程有拥有的资源
- 13 Linux进程的地址空间
- 14 大端模式与小端模式的区别,以及如何判断是大端模式还是小端模式?
- 15 C中静态全局变量与全局变量的区别?
1 如何查看一个进程内存使用的情况?
1-1 方式1:Linux平台可以使用top命令查看进程的内存使用情况。
PID:进程的ID PID(Process Identification)
USER:进程所有者
PR:priority值,进程的优先级别,越小越优先被执行
NI:nice值,值越大说明这个进程越“无私”,越不容易获得调度
VIRT:进程占用的虚拟内存
RES:进程占用的物理内存
SHR:进程使用的共享内存
S:进程的状态。S表示休眠,R表示正在运行,Z表示僵死状态,N表示该进程优先值为负数
%CPU:进程占用CPU的使用率
%MEM:进程使用的物理内存和总内存的百分比
TIME+:该进程启动后占用的总的CPU时间,即占用CPU使用时间的累加值。
COMMAND:进程启动命令名称
1-2 方式2:使用Linux的ps(program status [ˈsteɪtəs ] )命令
具体命令解释如下:
1)ps a 显示现行终端机下的所有程序,包括其他用户的程序。
2)ps -A 显示所有程序。
3)ps c 列出程序时,显示每个程序真正的指令名称,而不包含路径,参数或常驻服务的标示。
4)ps -e 此参数的效果和指定"A"参数相同。
5)ps e 列出程序时,显示每个程序所使用的环境变量。
6)ps f 用ASCII字符显示树状结构,表达程序间的相互关系。
7)ps -H 显示树状结构,表示程序间的相互关系。
8)ps -N 显示所有的程序,除了执行ps指令终端机下的程序之外。
9)ps s 采用程序信号的格式显示程序状况。
10)ps S 列出程序时,包括已中断的子程序资料。
11)ps -t<终端机编号>
指定终端机编号,并列出属于该终端机的程序的状况。
12)ps u
以用户为主的格式来显示程序状况。
13)ps x
显示所有程序,不以终端机来区分。
最常用的方法是ps -aux,然后再利用一个管道符号导向到grep去查找特定的进程,然后再对特定的进程进行操作。
2 Linux的一些其他问题
2-1 Linux下如何查找特定进程?
基本思想:组合使用ps命令与grep命令
ps -aux | grep 进程的关键信息
- grep: Global regular expression print
2-2 嵌入式项目的相关命令
insmod/rmmod(install module) 向内核加载/卸载驱动模块
知识点:
静态加载就是把驱动程序直接编译进内核,系统启动后可以直接调用。静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译和下载内核,效率较低。若采用静态加载的驱动较多,会导致内核容量很大,浪费存储空间。
动态加载利用了Linux的module特性,可以在系统启动后用insmod命令添加模块(.ko),在不需要的时候用rmmod命令卸载模块,采用这种动态加载的方式便于驱动程序的调试,同时可以针对产品的功能需求,进行内核的裁剪,将不需要的驱动去除,大大减小了内核的存储容量。
2-3 嵌入式系统的启动流程
bootloader(u-boot) --> Linux内核 --->内核挂载根文件系统 --->应用程序
bootloader主要完成一些硬件初始化工作,比如嵌入开发板中SDRAM与串口的设置
Linux内核:
内核根文件系统:在 Linux 中将一个文件系统与一个存储设备关联起来的过程称为挂载(mount),rootfs是基于内存的文件系统,所有操作都在内存中完成;也没有实际的存储设备,所以不需要设备驱动程序的参与。基于以上原因,linux在启动阶段使用rootfs文件系统,当磁盘驱动程序和磁盘文件系统成功加载后,linux系统会将系统根目录从rootfs切换到磁盘文件系统。
正常来说,根文件系统至少包括以下目录:
/etc/:存储重要的配置文件。
/bin/:存储常用且开机时必须用到的执行文件。
/sbin/:存储着开机过程中所需的系统执行文件。
/lib/:存储/bin/及/sbin/的执行文件所需的链接库,以及Linux的内核模块。
/dev/:存储硬件设备文件。
开机过程中仅有根目录会被挂载, 其他分区则是在开机完成之后才会持续的进行挂载的行为。就是因为如此,因此根目录下与开机过程有关的目录, 就不能够与根目录放到不同的分区去。那哪些目录不可与根目录分开呢?有底下这些:
/etc:配置文件
/bin:重要执行档
/dev:所需要的装置文件
/lib:执行档所需的函式库与核心所需的模块
/sbin:重要的系统执行文件
2-4 驱动设备的开发流程
驱动开发步骤?
-------------------------------------------------------------------------------------
通常驱动模块的开发都是采用动态加载的方式,从而方便调试,通过insmod命令可以安装设备文件(device file)
以字符类型的设备为例,包括LED灯以及按键等硬件设备。
1. 首先编写好底层驱动的函数,以LED等为例,读取灯的状态,写入灯的状态。
2. 然后就是 灯状态的读/写 与用户层的库函数read/write相关联。
3. 基本方法就是利用内核的文件提供的file operation结构体,将其中定义的read/write函数指针指向自己定义的
驱动函数。
4. 另外一点就是利用内核中module提供的函数定义模块初始化以及卸载函数,主要实现2个功能:
4.1 register_chrdev将实现的结构体注册到内核中,文件结构体的注册以及取消注册
4.2 IO物理地址映射到虚拟内存空间,以及取消映射。
然后编译为ko文件,内核加载这个文件,就能够看到其对应的设备文件。
5. 此时应用层可以使用标准的库函数去执行相关操作
------------------------------------------------------------------------------------
简单驱动代码分析(利用内核中的fs头文件中的file opertation结构体,三个成员变量分别是owner,open,write,其中open与write都是函数指针,存储自己定义的硬件操作函数地址)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h> // 定义了file operation结构
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *firstdrv_class;
static struct class_device *firstdrv_class_dev;
/*the class is used to create device files*/
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int first_drv_open(struct inode *inode,struct file *file);
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos);
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /*this is a macro*/
.open = first_drv_open,
.write = first_drv_write,
};
//通过系统提供的结构体实现标准读写打开对应的内核读写函数
int major;
/*parameters:main device number, device name, file operation*/
int first_drv_init(void)
{
major = register_chrdev(0, "first_drv", &first_drv_fops); //register kernel
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");//create device file: /
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
return 0;
}
int first_drv_exit(void)
{
unregister_chrdev(major,"first_drv"); //unload the device
class_device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class); //delete device file
iounmap(gpfcon);
return 0;
}
static int first_drv_open(struct inode *inode,struct file *file)
{
printk("first_drv_open
");
/*configure GPF4,5,6*/
*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
return 0;
}
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
printk("first_drv_write
");
int val;
copy_from_user(&val, buf, count); //transmit data from user buf to kernel,reverse use copy_to_user();
/*copy_from_user是系统内核提供的函数*/
if (val == 1) //turn on the light
{
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
}
else //ture off the light
{
*gpfdat |= (1<<4) | (1<<5) | (1<<6);
}
return 0;
}
module_init(first_drv_init); //use system macro to call the function
module_exit(first_drv_exit);
MODULE_LICENSE("GPL"); //allow using class to create device files automatically
2-5 Linux的netstat命令使用:
netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]
-t或--tcp 显示TCP传输协议的连线状况。
-u或--udp 显示UDP传输协议的连线状况。
netstat -l 显示监听的套接口
netstat -s 显示网络统计信息
netstat -g 显示组播关系
netstat -i 显示网卡列表
2-6 动态链接与静态链接的区别
动态链接库:。在Windows下,这些共享库文件就是.dll文件, 也就是Dynamic-Link Libary(DLL,动态链接在Linux下,这些共享库文件就是.so文件,也就是 Shared Object(一般我们也称之为动态链接库),驱动模块叫做Kernel Object
静态链接的优缺点(所有相关的东西都要打包):
- 优点: 简单,程序所依赖的东西都被打包了
- 缺点:浪费内存空间,会出现内存中有很多相同的公共库函数
动态链接的优缺点(只需要编译自己的那部分代码就行,程序执行时动态链接公共的库文件):
- 特点:系统需要映射一个主程序和多个动态链接模块,因此,相比于静态链接,动态链接使得内存的空间分布更加复杂。不同模块在内存中的装载位置一定要保证不一样。
ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。
Linux如何实现动态链接?
- 加载时符号重定位:把重定位的时机放到了动态库被加载到内存之后,由动态链接器来进行
- 地址无关代码(解决动态链接库的共享问题)
2-7 Linux open/read/write的底层原理
Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行I/O操作的系统调用都会通过文件描述符。
定义: 文件描述符可以理解为进程文件描述表这个表的索引,或者把文件描述表看做一个数组的话,文件描述符可以看做是数组的下标。当需要进行I/O操作的时候,会传入fd作为参数,先从进程文件描述符表查找该fd对应的那个条目,取出对应的那个已经打开的文件的句柄,根据文件句柄指向,去系统fd表中查找到该文件指向的inode,从而定位到该文件的真正位置,从而进行I/O操作。
https://zhuanlan.zhihu.com/p/143430585
理解Linux的文件描述符FD与Inode
3 进程线程以及协程的区别(拥有资源,系统开销,应用场景,相互关系)?
Liunx系统调用fork()
可以新建一个子进程,函数pthread()
可以新建一个线程。但无论线程还是进程,都是用task_struct
结构表示的,唯一的区别就是共享的数据区域不同
背后的核心思想:
- 时分复用共享:将时间分成多片,每一片作为不同的用途(每个程序或用户需要按照一定的顺序依次使用这个资源)
- 空分复用:多个程序或用户同时使用空间的的不同部分
基于这个思想:
-
多个进程可以看作对CPU时间等公共资源的时分复用,对内存资源的空分复用。(虚拟内存管理不仅仅时空分复用技术)
-
线程可以看作进程拥有的资源时分复用(CPU时间,公共资源)与空分复用(栈内存)。
- 比如Java的线程栈由多个栈帧组成,每个栈帧存储一些局部变量表,锁记录,返回地址等
-
协程可以看作线程拥有资源的时分复用(CPU时间,线程之间共享的公共资源)与空分复用(栈内存)。
3-1 线程与进程与线程的定义:
1) 进程:进程是资源调度的基本单位,或者说是可执行程序代码块的的一次执行。
- 线程提出的主要目的就是为了更加充分利用的资源。需要IO的场景多线程的进程比单线程的进程能够更加充分利用资源。
- 多进程使用场景:CPU密集型代码(各种循环处理、计算等等):
2)线程(轻量级进程):线程是程序执行的基本单位,每个进程中有且仅有一个的主线程,主线程和进程是相互依存,主线程结束进程也会结束。
-
多线程使用场景:IO密集型代码(文件处理、网络爬虫等)
-
多线程可以看作进程中的多个任务。
3)协程(Coroutine:用户态的轻量级线程):可以在一个线程了定义多个协程,通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,同一个线程内部最多只会有一个协程正在运行。
- 协程的应用场景:高并发的场景,比如TCP/HTTP/RPC服务、消息推送系统、聊天系统等
3-2 为什么协程适合高并发的场景?
更多的并发数必然要求线程需要频繁的切换以及更多的系统资源。
协程相比线程:
- 需要的资源更加的少,有限的资源下并发数量更多(Java中每个线程可能要分配几个MB栈空间,而协程只需要几KB的栈空间)
- 切换代价小,主要原因就在于不需要操作系统进入内核态。
[][理解Go协程与并发]理解GO的协程与并发
3-3 线程,进程,协程的区别
从拥有资源,系统开销以及通信来回答。
-
切换的过程
- 进程:用户态 ----> 内核态(CPU环境:栈,寄存器,文件句柄和页表信息) -------> 用户态
- 线程:用户态---->内核态(线程环境:一些寄存器与栈的内容)--->用户态
- 协程:不需要切换到内核态(切换时只需要保存寄存器与栈)
进程的切换过程(保存现场-->执行中断处理函数----->恢复现场)
3-4 Linux中父进程与子进程的关系,子进程与线程的关系
Linux父子进程基本概念:
- 所有进程都是 PID 为1的 init 进程的后代,系统中每个进程必有一个父进程
- Linux使用fork来创建子进程(新的进程要通过老的进程复制自身得到,这就是fork
fork的使用实例:
- fork()会返回2次,在父进程返回子进程的PID,在子进程则返回0
#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid;
int count=0;
fpid=fork(); /* step1: 调用fork,创建出子进程*/
/* ----------------step2: fork出来的进程只会执行fork之后的代码--------------*/
if (fpid < 0) // 子进程创建失败,父进程获取的返回值小于0
printf("创建进程失败!/n");
else if (fpid == 0) { // 子进程的执行到此处代码,由于返回值是0会打印
printf("我是子进程,由父进程fork出来/n");
count++;
}
else { // 父进程的执行到此处代码,由于返回值是0会打印
printf("我是父进程/n");
count++;
}
printf("统计结果是: %d/n",count);
return 0;
}
exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务。
- exec系列函数在执行时会直接替换掉当前进程的地址空间。
3-5 Linux下的copy-on-write机制实现以及应用场景
COW机制出现的原因?
- COW技术可减少分配和复制大量资源时带来的瞬间延时。
- COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
COW机制的原理:
- fork出的子进程共享父进程的物理空间(虚拟内存中他们有不同的虚拟空间),当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。
- fork出的子进程功能实现和父进程是一样的。如果有需要,我们会用
exec()
把当前进程映像替换成新的进程文件,完成自己想要实现的功能。
COW机制的高层理解:
写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
3-6 线程崩溃,进程是否崩溃?
线程崩溃是能够导致进程崩溃的
1)操作系统中进程与进程之间存在隔离性,由于进程拥有独立的虚拟地址地址空间(系统内核提供内存管理与维护,借助多级页表机制(64位是四级,32位二级)建立虚拟地址与实际地址的映射),线程之间由于共享进程的地址空间,因此没有隔离性
2)Java中线程崩溃,对应的虚拟机进程仍然可以执行,但是如果线程没有抛出异常的话,进程无法感知到线程是否崩溃。
参考资料
4 Linux进程之间通信的6种方式(InterProcess Communication,IPC)
4-1 管道(匿名管道,pipe)
全双工:在发送数据的时候能够接受,半双工的区别在于,同一时间只能有一个方向的传输。
定义:| 管道本质上是作为缓冲区的文件,文件的大小以及读写方式进行了限制,大小上为1页,即4K字节,在读写方式上当文件写满或者为空时则阻塞对应的读写进程。
- 无名管道只能用于具有亲缘关系的进程之间(父子关系),只能在父子进程间。经典的形式就是管道由父进程创建,进程fork子进程之后,就可以在父子进程之间使用了。
- 操作系统在内核创建一块缓冲区,并且为用户提供管道的操作句柄(文件描述符),用户通过IO接口实现管道的操作;描述符共有两个一个用于读数据,一个用于写数据;管道一个半双工的进程间通信方式
- 从管道读数据是一次性操作,数据一旦被读,它从管道中被抛弃,释放空间以便写更多的数据。
应用场景:把一个程序的输出直接连接到另一个程序的输入(半双工),
管道的结构:借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。
管道实现的深入理解:内核使用了锁(保证当前时刻只有读/写发生)、等待队列(用于存放阻塞的读/写的进程)和信号(通知读写)。
局限性:
- 只支持单向数据流;
- 只能用于具有亲缘关系的进程之间;
- 没有名字;
- 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
- 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;
总结:
- 半双工,即就是不能够同时在两个方向上传输数据,有的系统可能支持全双工。
- 匿名管道只能用于有亲缘关系的进程进行通信
- 命名管道可以用于任意进程间通信
4-2 FIFO(命名管道)
创建方法:
mkfifo <pipe-name>
特点:克服之前管道的缺点(只能用于具有亲缘概关系进程间的通信)而FIFO提供了一个路径名与之关联,以FIFO文件的形式存在于文件系统中,通过路径访问的方式,文件名存在文件系统中,而管道中的内容存在于内存中。可通过open、read、write(http://www.dzsc.com/product/searchfile/10366.html)对其操作;可以在不相关的进程间通信。只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out)。
优点:解决了系统在应用过程中产生的大量中间临时文件的问题,它可以被Shell调用使数据从一个进程到另一个进程,系统不必为该中间通道清理不必要的垃圾,或者释放该通道的资源,它可以被留作后来的进程使用。
4-3 消息队列(生产者与消费者)
优点:
缺点:数据量大,通信频繁不适合使用消息队列,拷贝太费时间。
各个进程通过队列标识符来利用内核提供的消息队列。
-
消息队列亦称报文队列,也叫做信箱。是Linux的一种通信机制,这种通信机制传递的数据具有某种结构,而不是简单的字节流。
-
消息队列的本质其实是一个内核提供的链表,内核基于这个链表,实现了一个数据结构
-
向消息队列中写数据,实际上是向这个数据结构中插入一个新结点;从消息队列汇总读数据,实际上是从这个数据结构中删除一个结点
-
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
-
消息队列也有管道一样的不足,就是每个数据块的最大长度是有上限的,系统上全体队列的最大总长度也有一个上限
4-4 信号量与信号
- 信号的本质是软件中断,提供了一种处理异步事件的方法,也是进程间唯一的异步通信方式。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:
第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。
第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。
第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
项目中信号的使用
中断的存在使得按键进程在没有按键按下时处于休眠状态。
4-5 共享内存
本质:将多个进程的部分逻辑地址空间映射到相同的物理地址。
-
每个进程通过shmat(Shared Memory Attach 绑定到共享内存块)
-
共享内存不保证同步,使用了信号量用来保证共享内存同步
优点:传输效率高
缺点:需要考虑内存的同步问题
共享内存与内存映射mmap的关系?
mmap系统调用并不完全是为了共享内存来设计的,它本身提供了不同于一般对普通文件的访问的方式,进程可以像读写内存一样对普通文件进行操作s,IPC的共享内存是纯粹为了共享。
总结:mmap系统调用是实现共享内存的一种方式。
4-6 UNIX域套接字
4-7 网络套接字
管道与共享内存的区别
管道和共享内存两者之间的区别:
- 管道需要在内核和用户空间进行四次的数据拷贝:由用户空间的buf中将数据拷贝到内核空间中 -> 内核空间将数据拷贝到内存中 -> 内存到内核 -> 内核到用户空间的buf。而共享内存则只拷贝两次数据:用户空间到内存 -> 内存到用户空间(不是太理解)。
- 管道用循环队列实现,连续传送数据可以不限大小。共享内存每次传递数据大小是固定的;
- 共享内存可以随机访问被映射文件的任意位置,管道只能顺序读写;
- 管道可以独立完成数据的传递和通知机制,共享内存需要借助其他通讯方式进行消息传递。也就是说,两者之间最大的区别就是: 共享内存区是最快的可用IPC形式,一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递,就不再通过执行任何进入内核的系统调用来传递彼此的数据,节省了时间。
原文链接
共享内存与消息队列的区别
共享内存的好处就是效率高,不需要太多次的进行数据的copy。可以直接进行读写内存。所以,相对来说在IPC进程间通信三大主题里面,共享内存要比消息队列使用多,而且消息队列只在有血缘关系的进程间通信;但是,共享内存不保证同步,使用了信号量用来保证共享内存同步。Linux中的两种共享内存。一种是我们的IPC通信System V版本的共享内存,另外的一种就是我们今天提到的存储映射I/O(mmap函数),当然还有一种POSIX的共享内存,它是在mmap基础之上构建的。
5 内存管理
概述:操作系统具有四个基本特征,并发,共享,虚拟与异步,提供了进程管理,内存管理,文件管理,输入输出管理(IO管理)。
内存管理的目标:
- 内存空间的分配与回收
- 地址转换(逻辑地址与物理地址的转换,也叫地址重定位)
- 内存空间的扩充(上下限寄存器策略或者重定位寄存器配合基地址寄存器)
- 存储保护
局部性原理:
- 时间局部性:部分命令和数据可能被反复执行和使用
- 基于时间局部性可以将最近用过的数据命令保存到高速缓存存储器。
- 空间局部性:某个局部的数据可能会被频繁访问
- 将一整块局部数据放入到比较大的高速缓存中。
5-1 虚拟/物理/交换/共享内存(重要)
物理内存(RAM):实际的物理内存,大小等于内存条的大小。
虚拟内存(个人理解):
- 广义上的虚拟内存是一种内存管理机制,系统并不直接为进程分配实际物理内存的地址空间,而是分配虚拟内存的连续地址空间。当程序实际执行时,在将虚拟内存空间通过地址转换映射到实际的内存地址空间(页机制使得离散的内存空间能够得以利用)
- 狭义的虚拟内存可以直接理解为虚拟存储器,就是一段虚拟的地址空间(注意这段虚拟的地址空间大小理论上不能超过内外存之和,也不能超出机器支持的地址范围)
交换内存(swap):交换内存的本质是一块专门的磁盘空间。这段磁盘空间配合内存的管理机制来实现内外存的页面交换(a hard drive partition that is dedicated to swapping)
共享内存:共享内存是一段被多个程序访问的内存,通常用于提供多个程序之间的通信,或者避免数据的冗余拷贝。
5-2 为什么要引入虚拟内存?
主要目的还是为了让更多的进程能够并发执行,如果运行的进程太多就需要更多的内存空间,虚拟内存扩充了内存,利用内存-外存二级存储架构让更多进程得以同时运行。
虚拟内存的大小理论上不能超过:
- 内存加外存的容量和
- 计算机的地址位数所能支持的最大空间
5-3 虚拟存储器的定义(基于局部性原理):
- 虚拟内存可以理解为一种基于离散内存分配的内存管理技术,使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间)。实际程序运行用的是离散的物理地址空间,还有可能一部分程序位于磁盘空间,当必要的时候才会交换到内存。(现象)比如Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。进程就可以很方便地访问虚拟内存。
- 虚拟存储技术形成了内存-外存的二级层次结构,局部性原理的存在使得系统让一个非常庞大的程序并不需要完全加载到内存就能够运行。
- 虚拟内存多个进程需要的虚拟内存共享实际物理内存,当有进程需要运行时,则通过地址映射将其对应的虚拟内存映射到实际内存。
5-4 虚拟存储器的分类:
- 页式虚拟存储器:虚拟空间与主存空间划分为大小相等的页,分别叫做虚页与实页。
- 虚地址与实地址的转换通过页表实现,页表常驻内存。
- 虚拟地址分为虚页号与页内地址,其中虚页号用于查找页表中的实地址。
5-5 虚拟地址的转换步骤(假定所需内容已装入主存)
-
S1:程序开始运行,给出所需要内容的虚拟地址,
-
S2: 取出页表基址寄存器存放的当前运行程序的 页表起始地址 与 虚页号 组合 成 页表项地址。 页表起始地址+虚页号=页表项地址
-
S3:根据 页表项地址 查询 页表 得到 实页号。
-
S4:将 实页号 与 页内地址 组合成 实地址。
- 实页号+页内地址=实地址
-
S5:CPU根据实地址访问内存单元取得需要的内容。
注意点: -
如果命中(访问的数据在内存中)只需访问二次内存,每个页表项中有 所对应虚页号, 实页号, 装入位等信息
-
在S3中如果查询页表发现装入位=0,则实页不在内存中,缺页中断,需进行页面替换与页表修改,访存次数大大增加。
-
段页式与页式一样与主存交换信息时的单位是页。段页式是逻辑结构分段,每段再划分为固定大小的页。
-
虚存对应用程序员透明,对系统程序员不透明。
5-6 Linux内存管理注意点:
-
虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同
-
Linux会为每个进程分配一段连续的虚拟地址空间。
6 中断与异常与软中断?
中断总体分为异步中断与同步中断:
-
异步中断通常是指硬件中断(屏蔽/不可屏蔽),包括外部中断(IO引脚的信号),定时器中断(时钟信号)这些由硬件支持的中断。这些中断的产生不受CPU这个硬件影响。
-
同步中断(异常,内中断):当指令执行时CPU控制单元产生的,之所以称为同步,CPU每执行完一条指令都会检查是否有中断产生。
- 异常分为错误(缺页异常的机制,CPU内部集成的MMU发出,有些ARM芯片MMU由另外的芯片支持,因此严格意义上错误不能简单的归为内中断),陷阱(Trap,也就是的软中断,通常由程序编写错误产生或者在程序中使用请求系统调用而引发的事件),和终止三种情况
7 Linux的文件描述符
- 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
- 在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。文件描述符是Linux/Unix系统特有的,其文件操作系统调用通常以文件描述符为参数
- 如果编写可移植的C程序,应尽量使用C标准库函数,C标准库函数的文件操作以系统调用为基础
8 Linux的虚拟文件系统(一切皆文件)
虚拟文件系统(Virtual File System,简称VFS)是Linux内核的子系统之一,它为用户程序提供文件和文件系统操作的统一接口,屏蔽不同文件系统的差异和操作细节。借助VFS可以直接使用open()
、read()
、write()
这样的系统调用操作文件,而无须考虑具体的文件系统和实际的存储介质。
目的:,这大大降低了操作文件和接入新文件系统的难度
9 Linux的用户/内核态,系统调用原理
-
系统调用这是用户态进程主动要求切换到内核态的一种方式。用户态进程通过系统调用申请使用操作系统提供的服务程序来完成工作;系统调用是通过终端机制来实现的。
-
异常当 cpu 在执行运行在用户态的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态;
-
外围设备的中断当外围设备完成用户请求的操作后,会向 CPU 发出相应的中断信号,这时 CPU 会暂停执行下一条即将要执行的指令而转到与中断信号对应的处理程序中去执行。如果前面执行的指令是用户态下的程序,那转换自然就是由用户态到内核态的转换;
库函数:对系统调用的封装,库函数根据不同的标准也有不同的版本,例如:glibc库,posix库等
shell:就是外壳的意思。就好像把内核包裹起来的外壳。它是一种特殊的应用程序,
系统调用:在X86下系统调用由0x80中断完成,而linux则使用int 0x80来触发所有的系统调用
如何区别不同系统调用的中断?
原本的中断程序的区分:在内核中,有一个数组称为中断向量表(Interrupt vector table),这个数组的第n项包含了指向第n号中断的中断处理程序的指针。当中断到来时,CPU会暂时中断当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成以后,CPU会继续执行之前的代码。
中断服务程序(内核代码)中会利用系统调用号区别不同的系统调用,从而获取对应的系统调用。
ARM对异常或者中断的处理过程(中断函数执行流程)
- 保存现场
- 处理中断
- 恢复现场
为什么系统调用有开销(不同系统调用开销是不同的)?
- 需要执行内核的代码,进程与线程的切换也需要调用系统调用,利用内核提供的函数进行切换。
10 Linux IO模式及 select、poll、epoll详解
10-1 Linux IO访问的机制
IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
数据读取方式 | 机制 |
---|---|
阻塞 I/O(blocking IO) | 进程在2个阶段完成前都是阻塞状态 |
非阻塞 I/O(nonblocking IO) | 用户进程需要不断的主动询问kernel数据好了没有 |
I/O 多路复用( IO multiplexing)包括(select,poll,epoll) | 但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的 |
信号驱动 I/O( signal driven IO) | |
异步 I/O(asynchronous IO) |
缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
Linux中的IO复用机制:IO复用机制就是可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
10-2 poll的原理
poll的实现依赖于内核驱动程序提供的等待队列。
step1:没有数据,则进程进入等待队列休眠。
step2:有数据,进程会触发中断,唤醒等待队列中的进程去读取数据。
其中poll_wait是调用poll_table初始化时赋值的__pollwait函数,该函数为__pollwait,把当前进程插入到该驱动提供的等待队列头中,最后数据到达时,一般会由该驱动来唤醒等待队列头中的进程
10-3 select的原理
// select的函数原型
/*
* @nfds: 待监听的最大fd值+1
* @readfds: 待监听的可读文件fd集合
* @writefds: 待监听的可写文件fd集合
* @exceptfds: 待监听的异常文件fd集合
* @timeout: 超时设置,在等待指定时间后返回超时
* return:返回满足条件的fd数量和,如果出错返回-1,如果是超时返回0
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
10-4 Linux中驱动程序如何让进程阻塞后在数据准备好唤醒?
通过一个叫做的等待队列的数据结构。
/* 定义一个等待队列,这个等待队列实际上是由当前驱动文件中对应的驱动提供的中断驱动的,
当中断发生时,会令挂接到这个等待队列的休眠进程唤醒 */
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static unsigned drivers_poll(struct file *file, poll_table *wait)
{
unsigned int mask = 0;
poll_wait(file, &button_waitq, wait); /* 将进程挂接到button_waitq等待队列下 */
/* 根据实际情况,标记事件类型 */
if (ev_press) // 判断是否有按键按下(数据是否准备好)
mask |= POLLIN | POLLRDNORM;
/* 如果mask为0,那么证明没有请求事件发生;如果非零说明有时间发生 */
return mask;
}
select的伪代码实现
/*
基本思想:遍历所有文件,如果有文件可读,那么select无需阻塞,直接返回即可,并标记该文件可读;否则,将调用进程加入到每个文件对应设备驱动的读等待队列中,并进入睡眠状态。
*/
count=0
FD_ZERO(&res_rset)
for fd in read_set
if(readable(fd))
count++
FDSET(fd, &res_rset)
break
else
add_to_wait_queue
if count > 0
return count
else
wait_any_event
return count
select的实现依赖于poll函数
poll函数的工作有两部分:
判断当前文件状态,并在返回值中标记。
对本驱动程序的等待队列调用poll_wait函数。
select的局限性(监听文件遍历,内存复制开销,文件有限制)
可以看出,这种实现效率之所以低下,有以下几方面原因:
可以同时监听的文件数量有限,最多1024个。这对要处理几十万并发请求的服务器来说,太少了!
每次调用select,都需要从0bit一直遍历到最大的fd,并且每隔32个fd还有调度一次(2次上下文切换)。试想,如果我要监听的fd是1000,那么该是多么的慢啊!而且在有多个fd的情况下,如果小的fd一直可读,那就会导致大的fd一直不会被监听。
内存复制开销。需要在用户空间和内核空间来回拷贝fd_set,并且要为每个fd分配一个poll_table_entry对象。
10-5 poll与select的区别?
select | 有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。 |
poll | poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的 |
epoll | 不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1) |
10-6 epoll的原理
核心思想:将“维护监视队列”和“进程阻塞”分离,,提供就绪列表避免轮询
poll机制之所以效率低下,有几个原因:
- 轮询与添加阻塞队列是绑定。
- Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升。
- Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表(高效的查找数据结构,eventpoll对象中就序列表Rdlist(Ready list) 的存在,)”,引用收到数据的 Socket,就能避免遍历。
eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。
- 绪列表引用着就绪的 Socket,所以它应能够快速的插入数据。程序可能随时调用 epoll_ctl 添加监视 Socket,也可能随时删除。当删除时,若该 Socket 已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,Epoll 使用双向链表来实现就绪队列
- 定义数据结构来保存监视的 Socket,至少要方便地添加和移除,还要便于搜索,以避免重复添加
epoll的LT和ET
10-7 select,epoll,poll的对比
12 select,epoll,poll的总结
epoll的优势:
- 支持大批量的文件描述符
- 查询效率高
- 数据传输快(通过mmap实现的共享内存)
为何支持百万并发
- 不用重复传递事件集合(返回一次)
- epoll初始化时,内核开辟了epoll缓冲区,缓冲区内事件以epitem结点挂载到红黑树上,通过epoll_ctl的任何操作都是O(logN)(红黑树高效的监控管理socket [ˈsɑːkɪt] 节点)
- epoll_wait调用仅需观察rdlist是否为空,若非空则拷贝rdlist到用户空间并返回触发事件数量,无需遍历
- 向内核中断处理注册回调,一旦关心的事件触发,回调自动将socket对应的epitem添加到rdlist中
高度的局域网环境中,epoll相比较其他IO复用机制,并不会显得有效率,相反
11 僵尸进程,孤儿进程?
如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。
12 Linux进程有拥有的资源
- 包括用于存放程序正文、数据的磁盘和内存地址空间,以及在运行时所需要的I/O设备,已打开的文件,信号量等
13 Linux进程的地址空间
14 大端模式与小端模式的区别,以及如何判断是大端模式还是小端模式?
低字节为低地址是小端模式,我们常用的x86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
15 C中静态全局变量与全局变量的区别?
- 静态全局变量只能在本文件内生效,即使使用extern也无法在其他c文件使用。
全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。
、