Linux系统

Linux系统编程

一、sleep,wait,delay的区别

1、Linux系统中sleep就是放弃当前cpu时间片,并阻塞指定时间;(与其他系统不同)

​ sleep() 则不会占住cpu资源,其他模块此时也可以使用cpu资源。所以,如果sleep(10),实际上延迟时间是要多于10s的,是一个不定的时间值,主要看cpu的运行情况;

2、Linux系统的wait()不是延时函数;

​ 其功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

​ 在Java系统中,wait()是延时函数,wait是进入等待池等待,让出系统资源,其他线程可以占用cpu,一般wait不会加时间限制,因为如果wait的线程运行资源不够,再出来也没用,要等待其他线程;

3、delay() 会占用cpu资源,导致其他功能此时也无法使用cpu资源。

​ delay函数是忙等待,占用CPU时间;而sleep函数使调用的进程进行休眠。

​ udelay(); mdelay(); ndelay();实现的原理本质上都是忙等待,ndelay和mdelay都是通过udelay衍生出来的。

二、物理内存和虚拟内存

物理内存:就是系统硬件提供的内存大小,是真正的内存,一般叫做内存条。也叫随机存取存储器(random access memory,RAM)又称作“随机存储器”,是与CPU直接交换数据的内部存储器,也叫主存(内存)。

虚拟内存:相对于物理内存,在Linux下还有一个虚拟内存的概念,虚拟内存就是为了满足物理内存的不足而提出的策略,它是利用磁盘空间虚拟出的一块逻辑内存,用作虚拟内存的磁盘空间被称为交换空间(Swap Space)。Linux会在物理内存不足时,使用虚拟内存,内核会把暂时不用的内存块信息写到虚拟内存,这样物理内存就得到了释放,这块儿内存就可以用于其他目的,而需要用到这些内容的时候,这些信息就会被重新从虚拟内存读入物理内存。

​ 在Linux中经常发现空闲的内存很少,似乎所有的内存都被消耗殆尽了,表面上看是内存不够用了,很多新手看到内存被“消耗殆尽”非常紧张,其实这个是因为Linux系统将空闲的内存用来做磁盘文件数据的缓存。这个导致你的系统看起来处于内存非常紧急的状况。但是实际上不是这样。这个区别于Windows的内存管理。Linux会利用空闲的内存来做cached & buffers。buffers是指用来给块设备做的缓冲大小(块设备的读写缓冲区);cached是作为page cache的内存, 文件系统的cache。你读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory也不会自动释放。这就会导致你在Linux系统中程序频繁读写文件后,你会发现可用物理内存会很少。其实这缓存内存(Cache Memory)在你需要使用内存的时候会自动释放,所以你不必担心没有内存可用。

三、I/O复用函数:select、poll、epoll之间的区别

​ select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

1、select函数

​ 该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

函数参数介绍如下:

(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2…maxfdp1-1均将被测试。因为文件描述符是从0开始的。

(2)中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

void FD_ZERO(fd_set *fdset);      //清空集合

void FD_SET(int fd, fd_set *fdset);  //将一个给定的文件描述符加入集合之中

void FD_CLR(int fd, fd_set *fdset);  //将一个给定的文件描述符从集合中删除

int FD_ISSET(int fd, fd_set *fdset);  // 检查集合中指定的文件描述符是否可以读写

(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

     struct timeval{

         long tv_sec;  //seconds

         long tv_usec; //microseconds

   };

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

2、poll函数

​ poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

# include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

​ (1)pollfd结构体定义如下:

struct pollfd {
	int fd;     	\* 文件描述符 *\
	short events;     \* 等待的事件 *\
	short revents;    \* 实际发生了的事件 *\
} ; 

​ (2)nfds代表需要监视文件描述符的个数;

​ (3)timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。

​ timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生;

​ timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

3、epoll函数

​ epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

(1) int epoll_create(int size);
  创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

​ 第一个参数是epoll_create()的返回值;

​ 第二个参数表示动作,用三个宏来表示:
​ EPOLL_CTL_ADD:注册新的fd到epfd中;
​ EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
​ EPOLL_CTL_DEL:从epfd中删除一个fd;

​ 第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

​ epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

4、总结

​ epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中类似)。

对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

所以:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

四、u-boot

1、u-boot基础

​ U-boot 一般是放在硬件“本地”(电路板)的Flash内,也有可能放在SD卡上,但是肯定不会放在内存或网络上。U-boot是嵌入式Linux操作系统中运行的第一个程序(可将U-boot和内核看做是两个不同的程序),其终极目的是引导加载内核进而使Linux操作系统运行起来。如果U-boot出现问题 ,操作系统就无法启动,所以对于U-boot而言,其稳定性是第一位,运行速度是排在第二位的。

​ u-boot是Bootloader的一种,不但依赖于CPU体系架构,还依赖于嵌入式系统板级设备。

2、u-boot的启动流程

(1)汇编阶段

A、初始化关键硬件关闭看门狗、中断、MMU和Cache(缓存)等,开启时钟、串口、Flash和内存等。
  目的:为了U-boot稳定性,关掉不必要或影响稳定性的硬件,打开运行U-boot必须的硬件。即通过使U-boot运行单纯化,从而保证U-boot的稳定性。

B、U-boot自搬移:U-boot自己将自己从Flash搬移到内存(RAM)运行。
  目的:提高U-boot的运行速度。因为内存要比Flash速度快。

(2)C语言阶段:

A、初始化大部分硬件;
B、将Linux内核(Kernel)从Flash中“搬移”到内存中运行;
C、运行内核(Kernel)。

3、u-boot移植

3.1准备编译

(1)下载u-boot源码,把源码cp到相应内核目录;

(2)解压u-boot源码,tar -xvf;

(3)更新库libssl-dev,sudo apt-get install libssl-dev;

3.2编译u-boot;

(1)进入u-boot源码目录;

(2)清除u-boot临时文件,make distclean;

(3)指定交叉编译工具,vi Makefile,添加相应内容;

(4)配置u-boot,源码目录中有一个board目录存放着不同厂家名称命名的相关目录文件,选择相应的文件make;

(5)编译u-boot,make;

3.3烧写u-boot

使用sdtool工具把制作完成的u-boot.bin文件拷贝到sdtool目录下,执行命令。

五、Linux内核

Linux内核的主要功能包括:进程管理、内存管理、文件管理、设备管理、网络管理;

1、Linux内核配置

为了正确、合理地设置内核编译配置选项,一般主要从四个方面考虑:

(1)尺寸小;自己定制内核可以使代码尺寸减小,运行会更快。

(2)节省内存;由于内核部分代码永远占用物理内存,定制内核可以使系统拥有更多的可用物理内存。

(3)减少漏洞;

(4)动态加载模块;

1.1内核配置

(1)下载相应版本的内核源码,cp到对应目录;

(2)tar -xvf,解压源码;

(3)清除内核临时文件,make distclean;

(4)内核配置命令有make config、make menuconfig、make xconfig,它们分别是字符接口、ncurses光标菜单和X-window图形窗口。

​ 到arch目录选择自己板子的内核文件配置cp到内核源码目录;

(5)指定交叉编译工具;

(6)make menuconfig选择模块编译进内核;

1.2内核编译

make uImage;需要在u-boot的tools目录中把mkimage文件cp到/usr/local/bin下,才能使用make uImage。

1.3编译设备树

六、Linux下创建进程的三种方式

​ 在Linux中主要提供了fork、vfork、clone三个进程创建方法。
​ 在linux源码中这三个调用的执行过程是执行fork(),vfork(),clone()时,通过一个系统调用表映射到sys_fork(),sys_vfork(),sys_clone(),再在这三个函数中去调用do_fork()去做具体的创建进程工作。

1、fork()

(1)fork函数所创建的子进程是父进程的完整副本,复制了父进程的资源,包括内存的task_struct内容。

(2)子进程拥有自己的虚拟地址空间,父子进程数据独有,**(写时复制)**代码共享。

(3)fork函数的返回值起到分流的作用,可以用fork的返回值判断哪个是父进程或子进程。

写时复制(copy-on-write):内核只为新生成的子进程创建虚拟空间结构,它们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应的段的行为发生时(或者父子进程对数据进行修改写操作时),再为子进程相应的段分配物理空间。

2、vfork()

(1)vfork函数相比fork函数更加粗暴,内核连子进程的虚拟地址都不创建了,而是直接共享父进程的,从而物理地址也就被理所当然的共享了。

(2)父进程会保证子进程先运行,在子进程调用exec(进程替换)或exit后才可能被调度运行。

fork和vfork的区别

(1)fork函数创建的子进程的虚拟地址空间是复制父进程的,在进行写操作之前,父子的物理页面是共享的,而当要进行写操作时,内核才会给要进行写操作的进程重新分配一个物理页面;而vfork函数的父子进程时共享虚拟地址,从而也共享了物理地址。换句话说,也就是fork函数复制父进程的数据段,代码段;而vfork函数父子进程共享数据段。

(2)fork函数父子进程执行的次序不确定,它是由调度器决定的;而vfork函数会保证子进程先运行,再被调用exit或exec后父进程才可能会运行。

(3)vfork 保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

3、clone()

​ 系统调用fork()和vfork()是无参数的,而clone()则带有参数。fork()是全部复制,vfork()是共享内存,而clone() 是则可以将父进程资源有选择地复制给子进程,而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的 clone_flags来决定。另外,clone()返回的是子进程的pid。

七、怎么取消父子进程的文件描述符共享?

​ 进程调用fork后,子进程和父进程的文件描述符所对应的文件表项是共享的,这意味着子进程对文件的读写直接影响父进程的文件位移量(反之同理)。

解决办法:

(1)进程中调用fd2 = dump(fd1) 产生的新的fd2所指向的文件表项和fd1指向的文件表项是相同的;

(2)在父子进程中分别调用:fd1 = open(“data.in”,O_RDWR); fd2 = open(“data.in”,O_RDWR); 那么fd1和fd2指向的文件表项是不同的。

(3)使用clone()函数选择性复制父进程的变量;

八、守护进程的实现流程

1、守护进程的特点

(1)在后台运行;
(2)不受任何终端所控制,不需要和用户进行交互;
(3)在系统引导的时候启动,直到系统关闭才结束;或者在有需要时启动,完成任务后自动关闭。

2、守护进程的编程流程

(1)fork() 复制出子进程,退出父进程

(2)setsid() 创建新的会话
setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制、让进程摆脱原控制终端的控制。

(3)chdir() 设置工作目录
由父进程所在的当前工作目录切换到其他工作目录,一般为/或者/tmp

(4)umask() 清除掩码
清除从父进程继承而来的文件创建掩码

(5)close() 关闭文件描述符
将从父进程继承来的所有打开的文件关闭

3、使用daemon()函数创建守护进程

#include <unistd.h>   
int daemon(int nochdir,int noclose)

创建守护进程的时,往往要做以下两件事情
(1)将进程的工作目录修改为"/"根目录
daemon的参数 nochdir为0时,即可将工作目录修改为根目录;
(2)将标准输入,输出和错误输出重定向到/dev/null
daemon的参数 noclose为0时,输入,输出以及错误输出重定向到/dev/null 。

#include <unistd.h>
int main(int argc, char *argv[])
{
    ...
    if (daemon(0, 0)) {//调用glibc库函数daemon,创建daemon守护进程
        perror("daemon");
        return -1;
    }
    //子进程内容
}

九、系统调用过程

​ 整个过程如下:首先指令流执行到系统调用函数时,系统调用函数通过int 0x80指令进入系统调用入口程序,并且把系统调用号放入%eax中,如果需要传递参数,则把参数放入%ebx,%ecx和%edx中。进入系统调用入口程序(System_call)后,它首先把相关的寄存器压入内核堆栈(以备将来恢复),这个过程称为保护现场。保护现场的工作完成后,开始检查系统调用号是不是一个有效值,如果不是则退出。接下来根据系统调用号开始调用系统调用处理程序(这是一个正式执行系统调用功能的函数),从系统调用处理程序返回后,就会去检查当前进程是否处于就绪态、进程时间片是否用完,如果不在就绪态或者时间片已用完,那么就会去调用进程调度程序schedule(),转去执行其他进程。如果不执行进程调度程序,那么接下来就会开始执行ret_from_sys_call,顾名思义,这这个程序主要执行一些系统调用的后处理工作。比如它会去检查当前进程是否有需要处理的信号,如果有则去调用do_signal(),然后进行一些恢复现场的工作,返回到原先的进程指令流中。至此整个系统调用的过程就结束了。

​ 系统调用本质上是应用程序请求OS内核完成某功能时的一种过程调用,它与一般的过程调用的几个差别:

1.运行在不同的系统状态

​ 一般的过程调用其调用程序和被调用程序运行在相同的状态——系统态或用户态,而系统调用最大的差别是:调用程序是运行在用户态,而被调用程序是运行在系统态。

2.状态的转换

​ 由于系统调用的调用和被调用过程是工作在不同的系统状态,因而不允许由调用过程直接转向被调用过程,需要通过软中断机制,先由用户态转换为系统态,经内核分析后,才能转向相应的系统调用处理程序。

3.返回问题

​ 在采用了抢占式(剥夺)调度方式的系统中,在被调用过程执行完成后,要对系统中所有要求运行的进程做优先权分析。当调用进程仍具有最高优先级时,才返回到调用进程进行继续执行;否则,将引起重新调度,以便优先权最高的进程优先执行。

4.嵌套调用

​ 像一般过程一样,系统调用也可以嵌套进行,即在一个被调用过程的执行期间,还可以利用系统调用命令去调用另一个系统调用。

十、让Linux变为实时操作系统

1、实时操作系统

实时操作系统的重要特性就是系统中的实时任务,要在一个可预期的时间范围内必须得到执行。当一个高优先级任务被唤醒执行,或主动执行时,他必须可以立即抢占其他任务,得到cpu的执行权,这段时间必须是可预期的。像我们所熟知的vxworks实时系统,可以做到10ns以内可预期。实时系统指系统要有确保的最坏情况下的服务时间,即对于事件的响应时间的截止期限是无论如何都必须得到满足

​ 衡量实时操作系统的重要指标:

​ (1)中断响应时间(可屏蔽中断)

​ 计算机接收到中断信号到操作系统作出响应,并完成切换转入中断服务程序的时间。对于抢先式内核,要先调用一个特定的函数,该函数通知内核即将进行

中断服务,使得内核可以跟踪中断的嵌套。抢先式内核的中断响应时间由下式给出:

中断响应时间=关中断的最长时间+保护CPU内部寄存器的时间+进入中断服务函数的执行时间+开始执行中断服务例程(ISR)的第一条指令时间.

​ (2)任务切换时间

2、Linux属于非实时操作系统

原因主要是:

(1)实时任务抢占时间是不可预期的。

(2)为什么抢占是不可预期的呢?这涉及到内核中的抢占点知识,其中**spin_lock(自旋锁)**锁,在unlock时是一个抢占点,但是spin_lock本身内部是不可以抢占的,这种api在内核中大量使用,事实上是spin_lock与spin_unlock之间临界区代码片段不可预期的。

3、提高Linux系统的实时性

​ kernel.org官方已经提供了相关补丁,我们只需要将开源linux内核打上rt补丁,就可以让linux变成实时操作系统。

​ 补丁原理:前面已经分析过,spin_lock锁会关掉cpu抢占调度,影响实时性。所以RT补丁将spin_lock锁变成可以抢占了,这样就不用等到unlock时才能调度到rt任务。

十一、字符设备和块设备的区别

1、字符设备

​ 字符设备按照字符流的方式被有序访问,像串口和键盘就都属于字符设备。如果一个硬件设备是以字符流的方式被访问的话,那就应该将它归于字符设备;反过来,如果一个设备是随机(无序的)访问的,那么它就属于块设备。

2、块设备

系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的设备被称作块设备,这些数据片就称作块。最常见的块设备是硬盘,除此以外,还有软盘驱动器、CD-ROM驱动器和闪存等等许多其他块设备。注意,它们都是以安装文件系统的方式使用的——这也是块设备的一般访问方式。

3、区别

​ (1)块设备通过系统缓存进行读取,不是直接和物理磁盘读取。字符设备可以直接物理磁盘读取,不经过系统缓存。

​ (2)这两种类型的设备的根本区别在于它们是否可以被随机访问——换句话说就是,能否在访问设备时随意地从一个位置跳转到另一个位置

十二、Linux signal编程

​ **软中断信号(signal,又简称为信号)**用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

​ 收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:

​ 第一种方法是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。

​ 第二种方法是忽略某个信号,对该信号不做任何处理,就象未发生过一样。

​ 第三种方法是对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。

1、sigaction函数

​ 函数原型:sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

signum:参数指出要捕获的信号;

act:参数指定新的信号处理方式;

oldact:参数输出先前信号的处理方式(如果不为NULL的话)。

2、 struct sigaction结构体

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}

​ **sa_handler **此参数和signal()的参数handler相同,代表新的信号处理函数
sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置;
sa_flags 用来设置信号处理的其他相关操作,下列的数值可用:
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL;
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用;
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,·· 内核将不会阻塞该信号。

3、signal() 函数

void (*signal(int sig, void (*func)(int)))(int)

sig :信号码。

func :一个指向函数的指针。它可以是一个由程序定义的函数,也可以是下面预定义函数之一:

SIG_DFL 默认的信号处理程序。
SIG_IGN 忽视信号。

4、增加信号的功能

void install_signal_handler()//捕捉信号,重置信号处理函数
{
	struct sigaction sig_action;
	sig_action.sa_handler = termination_handler;//信号处理函数
	sigemptyset(&sig_action.sa_mask);//初始化信号集合set,et设置为空。
	sig_action.sa_flags = 0;
	sigaction(SIGINT, &sig_action, NULL);
	sigaction(SIGHUP, &sig_action, NULL);
	sigaction(SIGTERM, &sig_action, NULL);
	sigaction(SIGSEGV, &sig_action, NULL);
} 
void termination_handler(int signum)
{
	printf("Got signal %d, tearing down all services\n",signum);
	...
    ...
	signal(signum, SIG_DFL);//恢复默认处理程序
}

十三、线程池

​ 把一堆开辟好的线程放在一个池子里统一管理,就是一个线程池。如果来了一个请求,我们从线程池中取出一个线程来处理,处理完了放回池内等待下一个任务,线程池的好处是避免了繁琐的创建和销毁线程的时间和资源,有效的利用了CPU资源。

实现原理是这样的:在应用程序启动之后,就马上创建一定数量的线程,放入空闲的队列中。这些线程都是处于阻塞状态,这些线程只占一点内存,不占用CPU。当任务到来后,线程池将选择一个空闲的线程,将任务传入此线程中运行。当所有的线程都处在处理任务的时候,线程池将自动创建一定的数量的新线程,用于处理更多的任务。执行任务完成之后线程并不退出,而是继续在线程池中等待下一次任务。当大部分线程处于阻塞状态时,线程池将自动销毁一部分的线程,回收系统资源。

创建线程池的流程:

(1)设置一个生产者消费者队列,作为临界资源;

(2)初始化n个线程,并让其运行起来,加锁去队列去任务运行;

(3)当任务队列为空时,所有线程阻塞;

(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程。为防止惊群效应,可以把所有阻塞的线程放入一个队列中,当任务来时,条件变量只唤醒队顶的线程。

十四、精简指令集和复杂指令集

RISC(精简指令集计算机)和CISC(复杂指令集计算机)是当前CPU的两种架构。它们的区别在于不同的CPU设计理念和方法。

1、精简指令集

常见的精简指令集微处理器包括AVR、PIC、ARM、DEC Alpha、PA-RISC、SPARC、MIPS、Power架构等。

2、复杂指令集

十五、虚拟文件系统

​ 虚拟文件系统也叫虚拟文件系统转换(Virtual Filesystem Switch,简称VFS),之所以说它虚拟,是因为该文件系统的各种数据结构都是随时建立或删除的,在盘上并不永久存在,只能存放在内存中。也就是说,只有VFS是无法工作的,因为它不是真正的文件系统。VFS是Linux 核心的一部分,其他内核子系统与VFS打交道,VFS又管理其他逻辑文件系统(各操作系统中的实际文件系统叫做逻辑文件系统)。所以VFS是文件系统和Linux 内核的接口,VFS以统一数据结构管理各种逻辑文件系统,接受用户层对文件系统的各种操作。虚拟文件系统是Linux内核的子系统之一,它为用户程序提供文件和文件系统操作的统一接口,屏蔽不同文件系统的差异和操作细节。借助VFS可以直接使用open()read()write()这样的系统调用操作文件,而无须考虑具体的文件系统和实际的存储介质。

1、VFS存在的意义

  1. 向上,对应用层提供一个标准的文件操作接口

  2. 对下,对文件系统提供一个标准的接口,以便其他操作系统的文件系统可以方便的移植到Linux上

  3. VFS内部则通过一系列高效的管理机制,比如inode cache, dentry cache 以及文件系统的预读等技术,使得底层文件系统不需沉溺到复杂的内核操作,即可获得高性能;

  4. 此外VFS把一些复杂的操作尽量抽象到VFS内部,使得底层文件系统实现更简单

十六、互斥量与二值信号量

1、二值信号量

​ 用于任务同步和中断同步,也可以实现互斥访问,但不具有优先级继承优先级反转请看目录二十。

2、互斥量

​ 互斥信号量简单说是具有优先级继承的二值信号量,用于进程中的互斥,不可用于中断中(1:具有优先级继承机制,2:中断服务函数不能因为等待互斥信号量而阻塞)

互斥信号量优先级继承机制:当一个低优先级的任务正在使用这个互斥信号量时,高优先的任务在等待这个互斥信号量的时候,高优先级的任务会将低优先级的任务的优先级提到和自己同一个水平,从而来避免位于低优先和高优先的任务抢占cpu时间运行,而导致次优先级任务先于高优先级的任务运行,引起优先级翻转。

3、互斥量与信号量的区别

(1)互斥量用于线程的互斥,信号量用于线程的同步。

互斥 是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步 是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。

(2)互斥量无法保证线程对资源的有序访问,信号量可以。

(3)互斥量值只能为0/1,信号量值可以为非负整数。

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。

(4)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

十七、调用函数前未声明会出现什么情况?

​ 在函数调用之前,要求声明,这是为了告诉编译器函数返回值的类型,函数接受参数的类型和个数。

#include <stdio.h>
int main(void)
{
	int c;
     c = sun();
	printf("%d",c);
	return 0;
}
int  sun()
{
	return 3;
}

结果是3,同时:warning C4013: “sun”未定义;假设外部返回 int。

如果再改下一些代码

#include <stdio.h>
int main(void)
{
	float c;
      c = sun();
	printf("%f",c);
	return 0;
}
float  sun()
{
	return 3.14;
}

在调用sun的时候,编译器不知道sun函数的类型,参数,所以编译器默认函数的返回值是整型值,本来3.14是以浮点数存储的,其二进制为01000000010010001111010111000011,于是把这个浮点数方式的存储的二进制当成整数来解读,所以值为-858993472,然后再因为c是float型,所以最后的结果是-858993472.000000(在%f下,默认保存小数点后6位)。

十八、Linux相比于Windows的开发优势

1、Linux免费开源;

2、Linux的命令行操作方式,执行命令简单直接;没有消耗内存资源的GUI,系统崩溃的概率也低;

3、gcc, Makefile强制程序员更加了解程序的编译运行,以及库的链接和载入;

4、gdb可以在命令行调试;

5、强大的shell脚本以及swk、sed等工具;

6、环境配置方便。

十九、静态重定位和动态重定位

1、地址重定位

​ 就是操作系统将逻辑地址转变为物理地址的过程。也就是对目标程序中的指令和数据进行修改的过程。

将逻辑地址空间重定位到物理地址空间的时机有三种:

​ 1、程序编译连接时:符号绑定,各Obj模块的相对虚拟地址空间 -->统一的虚拟地址空间;可指定一个BaseAddress以优化装载时的重定位。

​ 2、程序装入内存时: 虚拟地址空间 -->虚拟地址空间;如果映像文件中的Base Address 与实际装载的起始虚拟地址空间,不一致,则需要根据偏移量 重定位 重定位项表。

​ 3、程序执行时:虚拟地址空间 -->物理地址空间, MMU。

2、静态重定位:程序装入内存时

静态重定位是在程序执行之前进行重定位,它根据装配模块将要装入的内存起始位置,直接修改装配模块中的有关使用地址的指令。

静态重定位,在逻辑地址转换为物理地址的过程中,地址变换是在进程装入时一次完成的,以后不再改变。

优点:是无需增加硬件地址转换机构,便于实现程序的静态连接。在早期计算机系统中大多采用这种方案。

​ **缺点:**1)程序重定位之后就不能在内存中搬动了;2)要求程序的存储空间是连续的,不能把程序放在若干个不连续的区域中。

3、动态重定位:程序执行时

动态重定位:动态运行的装入程序把转入模块装入内存之后,并不立即把装入模块的逻辑地址进行转换,而是把这种地址转换推迟到程序执行时才进行,装入内存后的所有地址都仍是逻辑地址。这种方式需要寄存器的支持,其中放有当前正在执行的程序在内存空间中的起始地址。

优点:内存空间可以移动;各个用户进程可以共享内存中同一程序的副本。

缺点:增加了机器成本,而且实现存储管理的软件算法比较复杂。

二十、优先级反转

1、优先级反转出现的原因

​ 优先级反转,是指在使用信号量时,可能会出现的这样一种不合理的现象,即:

高优先级任务被低优先级任务阻塞,导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。-- 从现象上来看,好像是中优先级的任务比高优先级任务具有更高的优先权。二值信号量)。

​ 具体来说:当高优先级任务正等待信号量(此信号量被一个低优先级任务拥有着)的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行类似死锁的情形发生。

​ 一个具体的例子:
​ 假定一个进程中有三个线程Thread1(高)、Thread2(中)和Thread3(低),考虑下图的执行情况。

  • T0时刻,Thread3运行,并获得同步资源SYNCH1;
  • T1时刻,Thread2开始运行,由于优先级高于Thread3,Thread3被抢占(未释放同步资源SYNCH1),Thread2被调度执行;
  • T2时刻,Thread1抢占Thread2;
  • T3时刻,Thread1需要同步资源SYNCH1,但SYNCH1被更低优先级的Thread3所拥有,Thread1被挂起等待该资源
  • 而此时线程Thread2和Thread3都处于可运行状态,Thread2的优先级大于Thread3的优先级,Thread2被调度执行。最终的结果是高优先级的Thread1迟迟无法得到调度,而中优先级的Thread2却能抢到CPU资源。

上述现象中,优先级最高的Thread1要得到调度,不仅需要等Thread3释放同步资源(这个很正常),而且还需要等待另外一个毫不相关的中优先级线程Thread2执行完成(这个就不合理了),会导致调度的实时性就很差了。

2、避免办法:优先级继承

​ 优先级继承就是为了解决优先级反转问题而提出的一种优化机制。其大致原理是让低优先级线程在获得同步资源的时候(如果有高优先级的线程也需要使用该同步资源时),临时提升其优先级。以前其能更快的执行并释放同步资源。释放同步资源后再恢复其原来的优先级。(互斥量)

上一篇:错误内存【读书笔记】C程序中常见的内存操作有关的典型编程错误


下一篇:I/O模型之二:Linux IO模式及 select、poll、epoll详解