系统编程寻找心的巨人

1)总线:贯穿整个系统的一组电子管道,携带信息字节并负责在各个部件间传递。通常总线被设计成传送定长的字节块,各种系统的字节数不相同,例如IntelPentium系统的字节长为4字节。

2)I/O设备:I/O(输入/输出)设备是系统与外界的联系通道,主要包括:键盘和鼠标,显示器以及用于长期存储数据和程序的磁盘驱动器。每一个I/O设备都是通过一个控制器与适配器与I/O设备连接起来的。控制器是主板上的芯片组,而适配器则是一块插在主板插槽上的卡。

3)主存:主存是由一组DRAM(动态随机访问存储器)芯片组成的。在处理器执行程序时,它被用来存放程序和程序处理的数据。

4)处理器中央处理单元(CPU):简称处理器,是解释存储在主存中指令的引擎。处理器的核心是程序计数器(PC)的字长大小的存储设备(或寄存器)。PC指向主存中的某条机器语言指令(内含其地址)。

5)高速缓存:之前但系统在执行hello程序时会有大量的拷贝工作,例如把代码和数据从磁盘拷贝到主存,从主存拷贝到寄存器堆,再从寄存器堆把文件拷贝到显示设备中。这些拷贝工作会减慢程序的实际工作。因此,为了加快程序运行速度,系统设计者采用了更小更快的存储设备,称为高速缓存存储器,它们被用来作为暂时的集结区域,存放处理器在不久的将来可能会需要的信息。

6)形成层次结构的存储设备:存储器分层结构的主要思想是一个层次上的存储器作为下一层次上的存储器的高速缓存。

图1.3一个存储器层次结构的示例

对于一个hello.c程序,从源文件到目标文件的转化是由编译器驱动程序(compilerdriver)完成的,翻译过程分为四个阶段完成,执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统。

图1.1编译系统

预处理阶段:预处理器(cpp)根据以字符#开头的命令修改原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。

编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。

汇编阶段:汇编器(a.s)将hello.s翻译成机器语言指令,并把指令打包成为一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。

链接阶段:此时hello程序调用了printf函数。printf函数存在于一个名为printf.o的单独的预编译目标文件中。链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。最后可执行文件加载到存储器后由系统负责执行。

◆使用gcc编译C程序的详细过程

在Linux中,gcc更像一个工具大管家,管理很多工具一起来对C程序进行编译。详细过程请看下图,带阴影的箭头表示文件的流程,空白箭头表示控制过程:

下面详细介绍一下这个过程。

1、程序员在Linux终端中输入命令gcceatc.c–oeatc

2、gcc接管Linux的控制权,然后立即启用一个工具Cpreprocessor(cpp)。这个工具处理C语言的源代码文件(eatc.c),处理比如引用#include、#define相应的替换之类的东西(预处理)。

3、之后,gcc接管。gcc把预处理后的文件进一步处理(进行语法编译),编译通过则变成和原始的C文件等价的.s汇编文件,这是一个人工可以读懂的文件。

4、之后,gcc就省事了。它把.s文件交给gas(一种GNUassembler)进行处理,生成.o文件(即:二进制文件)。

5、之后,再使用ld(一种GNUlinker)进行处理,把文件中使用到的C库程序全部都链接到一起。最终形成一个可执行文件

(.o文件)。

6、gcc把控制权交还给Linux。

详细解析:

C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织,形成最终生成可执行代码的过程。过程图解如下:

从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。

编译过程又可以分成两个阶段:编译和汇编。

编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段:

读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。

伪指令主要包括以下四个方面:

1)宏定义指令,如#defineNameTokenString,#undef等。

对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。

2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。

这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

3)头文件包含指令,如#include"FileName"或者#include等。

采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。

包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。

4)特殊符号,预编译程序可以识别一些特殊的符号。

例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输入而被翻译成为机器指令。

经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\等等。

编译程序所要作的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。

对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

目标文件由段组成。通常一个目标文件中至少有两个段:

1)代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。

2)数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

UNIX环境下主要有三种类型的目标文件:

1)可重定位文件

其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

2)共享的目标文件

这种文件存放了适合于在两种上下文里链接的代码和数据。

第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;

第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

3)可执行文件

它包含了一个可以被操作系统创建一个进程来执行的文件。

汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

1)静态链接

2)动态链接

在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

我们在linux使用的gcc编译器便是把以上的几个过程进行捆绑,使用户只使用一次命令就把编译工作完成,这的确方便了编译工作,但对于初学者了解编译过程就很不利了,下图便是gcc代理的编译过程:

从上图可以看到:

1)预编译

将.c文件转化成.i文件

使用的gcc命令是:gcc–E

对应于预处理命令cpp

2)编译

将.c/.h文件转换成.s文件

使用的gcc命令是:gcc–S

对应于编译命令cc–S

3)汇编

将.s文件转化成.o文件

使用的gcc命令是:gcc–c

对应于汇编命令是as

4)链接

将.o文件转化成可执行程序

使用的gcc命令是:gcc

对应于链接命令是ld

总结起来编译过程就上面的四个过程:预编译处理(.c)-->编译、优化程序(.s、.asm)-->汇编程序(.obj、.o、.a、.ko)-->链接程序(.exe、.elf、.axf等)。

C语言编译的整个过程是非常复杂的,里面涉及到的编译器知识、硬件知识、工具链知识都是非常多的,深入了解整个编译过程对工程师理解应用程序的编写是有很大帮助的,希望大家可以多了解一些,在遇到问题时多思考、多实践。

一般情况下,我们只需要知道分成编译和链接两个阶段,编译阶段将源程序(*.c)转换成为目标代码(一般是obj文件,至于具体过程就是上面说的那些阶段),链接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件(exe文件)就可以了,其他的都需要在实践中多多体会才能有更深的理解。

----------------------------------华丽的分割线--------------------------------

◆Cpu与外设的三种交互方式(I/O通信技术)

1)CPU的轮循

2)中断数据传输一直向CPU申请中断?

3)DMA直接内存访问例如:摄像头

◆一段代码中的量在内存中的地址分配:

(高地址)

栈(<4M)局部变量形参函数的返回值地址由上到下生长

堆调用mallocnew来产生地址由下到上生长

数据段常量、全局变量、静态变量

代码段main函数的入口代码的处理

(低地址)

◆栈溢出:

intf(intx)

{

inta[10];

a[11]=x;

}

这个就是栈溢出,x被写到了不应该写的地方。在特定编译模式下,这个x的内容就会覆盖f原来的返回地址。也就是原本应该返回到调用位置的f函数,返回到了x指向的位置。一般情况下程序会就此崩溃。但是如果x被有意指向一段恶意代码,这段恶意代码就会被执行。

◆可执行文件加载到内存后形成的进程在内存中的结构

首先要来理解一下可执行文件加载进内存后形成的进程在内存中的结构,如下图:

代码区:存放CPU执行的机器指令,代码区是可共享,并且是只读的。

数据区:存放已初始化的全局变量、静态变量(全局和局部)、常量数据。

BBS区:存放的是未初始化的全局变量和静态变量。

栈区:由编译器自动分配释放,存放函数的参数值、返回值和局部变量,在程序运行过程中实时分配和释放,栈区由操作系统自动管理,无须程序员手动管理。

堆区:堆是由malloc()函数分配的内存块,使用free()函数来释放内存,堆的申请释放工作由程序员控制,容易产生内存泄漏。

◆C语言中数据类型存储的比较

c语言中的存储类型有auto,extern,register,static这四种,存储类型说明了该变量要在进程的哪一个段中分配内存空间,可以为变量分配内存存储空间的有数据区、BBS区、栈区、堆区。下面来一一举例看一下这几个存储类型:

1.auto存储类型

auto只能用来标识局部变量的存储类型,对于局部变量,auto是默认的存储类型,不需要显示的指定。因此,auto标识的变量存储在栈区中。示例如下:

#include

intmain(void)

autointi=1;//显示指定变量的存储类型

intj=2;

printf("i=%d\tj=%d\n",i,j);

return0;

2.extern存储类型

示例如下:

inti=5;//定义全局变量,并初始化

voidtest(void)

printf("insubfunctioni=%d\n",i);

printf("inmaini=%d\n",i);

test();

$gcc-otesttest.cfile.c#编译连接

$./test#运行

结果:

inmaini=5

insubfunctioni=5

3.register存储类型

registerinti,sum=0;

for(i=0;i<10;i++)

sum=sum+1;

printf("%d\n",sum);

4.static存储类型

intsum(inta)

autointc=0;

staticintb=5;

c++;

b++;

printf("a=%d,\tc=%d,\tb=%d\t",a,c,b);

return(a+b+c);

intmain()

inti;

inta=2;

for(i=0;i<5;i++)

printf("sum(a)=%d\n",sum(a));

$gcc-otesttest.c

$./test

a=2,c=1,b=6sum(a)=9

a=2,c=1,b=7sum(a)=10

a=2,c=1,b=8sum(a)=11

a=2,c=1,b=9sum(a)=12

a=2,c=1,b=10sum(a)=13

5.字符串常量

char*a="hello";

voidtest()

char*c="hello";

if(a==c)

printf("yes,a==c\n");

else

printf("no,a!=c\n");

char*b="hello";

char*d="hello2";

if(a==b)

printf("yes,a==b\n");

printf("no,a!=b\n");

if(a==d)

printf("yes,a==d\n");

printf("no,a!=d\n");

yes,a==b

yes,a==c

no,a!=d

总结如下表:

两大周期:取指周期和执行周期

◆程序代码在CPU中的执行过程:

分析过程:以上指向的代码是y=x+y;

这里存储器就是内存

◆DMA(directmemoryaccess)

过程分析:主存RAM和IO之间的大规模的数据传递

CPU

I/O

RAM

HRQ

DREQ

1、申请定位通道(I/ODMACCPU)

2、确定内存读写方向(开始读写的存储单元)

3、确定内存读写地址(IO设备的地址)

4、确定数据的大小

5、

让出总线控制权

处理中断程序

2、3、4:CPU让出总线控制权CPUDMACIO

5:DMAC把总线控制权交给CPU,CPU执行一段检查本次DMA传输操作正确性的代码。最后,带着本次操作结果及状态继续执行原来的程序。

◆操作系统分时多任务操作系统的执行过程:

Cpu中只有一个程序其他就绪程序都在主存储器(mainmemory)

◆cache高速缓冲存储器

Cache(一二级)

Cache(硬盘缓冲)

CPU内存硬盘

1、存在于主存与CPU之间的一级存储器

2、由静态存储芯片(SRAM)组成

3、容量比较小但速度比主存高得多接近于CPU的速度。

主要由三大部分组成:

1、Cache存储体:存放由主存调入的指令(I-cache)与数据块(D-cache)。

2、地址转换部件:建立目录表以实现主存地址到缓存地址的转换。

3、替换部件:在缓存已满时按一定策略进行数据块替换,并修改地址转换部件。

◆中断产生的详细过程:

解析:

1、对于操作系统来说中断结束后不一定执行原来的程序,要看此时有没有调度这个程序。

2、PSW:程序状态寄存器,是计算机系统的核心部件——控制器的一部分,存储两种状态:1、状态信息,如:有无溢出2、控制信息,如:是否允许中断

3、PC:程序计数器,存放指令的地址。

4、处理器将PSW和PC压入控制栈,就是将中断的信息和程序执行的位置保存下来,然后根据相应信息去加载新的PC值,

◆内核运行的两种状态

中断上下文:中断时,内核不代表任何进程运行,一般不访问当前进程的数据结构,此时的上下文称中断上下文

进程上下文:陷入(或异常)到内核时,此时内核代表某个进程运行,一般要访问进程的数据结构,此时的上下文称进程上下文

◆详细解析进程上下文和中断上下文

进程上下文和中断上下文是操作系统中很重要的两个概念,这两个概念在操作系统课程中不断被提及,是最经常接触、看上去很懂但又说不清楚到底怎么回事。造成这种局面的原因,可能是原来接触到的操作系统课程的教学总停留在一种浅层次的理论层面上,没有深入去研究。处理器总处于以下状态中的一种:1、内核态,运行于进程上下文,内核代表进程运行于内核空间;2、内核态,运行于中断上下文,内核代表硬件运行于内核空间;3、用户态,运行于用户空间。

用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

关于进程上下文LINUX完全注释中的一段话:

当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够务必得到切换时的状态执行下去。在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。

正是有了不同运行状态的划分,才有了上下文的概念。用户空间的应用程序,如果想要请求系统服务,比如操作一个物理设备,或者映射一段设备空间的地址到用户空间,就必须通过系统调用来(操作系统提供给用户空间的接口函数)实现。

通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行,所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容,当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。

同理,硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。

Linux内核工作在进程上下文或者中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文;另一方面,中断处理程序,异步运行在中断上下文。中断上下文和特定进程无关。

运行在进程上下文的内核代码是可以被抢占的(Linux2.6支持抢占)。但是一个中断上下文,通常都会始终占有CPU(当然中断可以嵌套,但我们一般不这样做),不可以被打断。正因为如此,运行在中断上下文的代码就要受一些限制,不能做下面的事情:

1、睡眠或者放弃CPU

这样做的后果是灾难性的,因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉

2、尝试获得信号量

如果获得不到信号量,代码就会睡眠,会产生和上面相同的情况

3、执行耗时的任务

4、访问用户空间的虚拟地址

因为中断上下文是和特定进程无关的,它是内核代表硬件运行在内核空间,所以在中断上下文无法访问用户空间的虚拟地址。

◆linux硬件中断处理

今天看了0.11核的关于硬件中断处理的基本原理,下面作一下总结:

一、I386中断处理原理

I386体系结构CPU中有两种中断,硬中断和软中断,硬中断是外部硬件产生的,软中断是程序中的某条指令或者程序对标志寄存器中某个标志的设置而产生的,与硬件电路无关。无论是硬件中断和软件中断都有各自的中断对应的处理程序,这些处理程序分布在内核中。那么系统是怎么根据不同的中断找到对应的处理过程的呢?当发生一个中断时候,如果是硬件中断,则CPU会再执行两个周期从对应的硬件中取到中断号,如果是软中断,则直接从指令中取得中断号,CPU再根据中断号在中断向量表中找到对应的中断处理程序的入口,并调用处理。在调用这些处理程序之前,会保护一些寄存器的值,这分为两种情况处理:

1)未发生特权级改变。这种情况下,当前进程是内核进程,由于中断处理程序也在内核中,所以程序的转移不会引起特权级变化。这个时候,CPU会将当前进程的EFLAGS、CS、IP压入堆栈,处理程序结束的时候,iret指令会从堆栈中恢复这些寄存器值。

2)特权级发生变化。这种情况下,当前进程是用户进程,由于中断处理程序在内核中,程序需要转移到内核的代码段和数据段,这个时候就会发生特权级的转变:从用户态专项核心态。这个时候要在将当前进程的EFLAGS、CS、IP压入堆栈之前,把原进程的SS、ESP也压入堆栈,因为,特权级的转变会引起堆栈的切换。处理程序结束的时候,iret指令会从堆栈中恢复这些寄存器的值。

二、Linux中断处理原理

CPU只是提供了发生中断时候,具体是怎么找到中断处理程序的而中断向量表的设置和中断处理过程需要操作系统提供。在Linux中,中断处理程序的描述如入口,也就是上面说的中断向量是中断门和陷阱门的形式,对应的中断向量表叫做中断描述符表IDT。

1)IDT的设置

IDT的设置是在main.c中初始化中调用trap_init()函数完成的,trap_init()在trap.c中定义,主要是调用set_trap_gate和set_system_gate填充中断号对应在IDT中的中断门和陷阱门。这两个函数的具体定义和实现在system.c中。

2)中断处理程序

Linux中的中断处理程序主要包括两个文件,asm.s和trap.c,asm.s用于实现大部分硬件异常所引起的中断的汇编处理过程。而trap.c则实现了asm.s的中断处理过程中调用的C函数。还有几个中断处理程序的实现在system_call.s和page.s中。

A、asm.s

该程序的主要处理方式是调用trap.c中对应的C函数,显示出错位置和出错码,然后退出中断。其中的处理分为两种情况,带出错号的中断处理和不带出错号的处理。

对于不带出错号的处理过程是这样的,在各自的不带中断号的处理过程中,将对应的C函数指针入栈,然后跳到no_error_code处,no_error_code做到工作是:

交换eax和栈顶值,目的是保存eax的值同时将函数指针值放入eax;

保护原进程的通用寄存器:将ebx,ecx,edx,edi,esi,ebp,ds,es,fs入栈;

将错误码入栈(均为0);

指向中断返回地址eip的栈指针esp的值入栈;

重新设置ds、es、fs。使其指向内核段。

调用C函数;

退栈,恢复各个寄存器的值,最后iret。

带中断号的处理过程与不带中断号的处理过程的不同有三,一个地方是在各自带中断号的处理层过程中,跳到error_code而不是no_error_code。再一个地方是开始的地方要做两次交换,一次是错误码和eax交换,一次使函数地址和ebx的交换。另一个地方是错误码入栈的是对应实际的错误码,而不是0。

B、trap.c

此程序包括一些asm.s中调用的C函数的定义和实现,并显示错误位置和错误码。还有一个就是IDT的初始化:trap_init()的定义和实现。在0.11核中,很多函数中基本上都是空的。

◆关于linux虚拟内存的管理策略:

关于进程及其内存分配

进程的概念:

一个正在执行的程序

一个正在计算机上执行的程序实例

能分配给处理器并有处理器执行的实体

具有以下特征的活动单元

一组执行的指令序列

一个当前状态

首先要明白一个概念:进程中使用的所有地址都是虚地址。

linux中进程可运行在用户态和内核态。

用户态:线性地址位于0~3G范围内(从虚拟地址0x000000000xBFFFFFFF)

内核态:线性地址范围为3G~4G(从虚拟地址0xC0000000到0xFFFFFFFF);

◆深入理解C程序内存布局

1、堆和栈的区别,堆和栈的最大限制堆主要用来分配动态内存,操作系统提供了malloc等内存分配机制来供程序员进行堆内存的分配,同时,堆内存的释放需要程序员来进行。malloc分配的是虚拟地址空间,和用到的实实在在的物理内存是两码事,只有真正往空间里写东西了,os内核会触发缺页异常,然后才真正得到物理内存。32位Linux系统总共有4G内存空间,Linux把最高的1G(0xC0000000-0xFFFFFFFF)作为内核空间,把低地址的3G(0x00000000-0xBFFFFFFF)作为用户空间。malloc函数在这3G的用户空间中分配内存,能分配到的大小不超过3G,需除去栈、数据段(初始化及未初始化的)、共享so及代码段等占的内存空间。堆的地址空间是由低向高增长的(先分配低地址)。我用以下程序进行测试:

点击(此处)折叠或打开

1.#include

2.#include

3.

4.intmain(intargc,char*argv[])

5.{

6.char*ch=NULL;

7.unsignedintsize=2147*1000000;

8.ch=(char*)malloc(size);

9.if(ch==NULL)

10.perror("mallocfailed\n");

11.else

12.printf("mallocsuccess\n");

13.free(ch);

14.return0;

16.}

发现最大能分配的内存约为2.147GB。为什么这么说:我们可以看malloc函数的原型void*malloc(size_tsize);size_t在stddef.h里定义的是unsignedint类型,故在ilp32平台上其最大取值是2147483647栈由编译器自动分配释放,存放函数的参数值,局部变量的值等。栈的地址空间是由高向低减少的(先分配高地址)。在Linux中,用ulimit-a命令可以看到栈的最大分配空间(stacksize)是8192kB,即8MB多。2、Linux中进程最大地址空间Linux的虚拟地址空间也为0~4G。Linux内核将虚拟的4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为"内核空间"。将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为"用户空间"。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

其中,很重要的一点是虚拟地址空间,并不是实际的地址空间。进程地址空间是用多少分配多少,4G仅仅是最大限额而已。往往,一个进程的地址空间总是小于4G的,你可以通过查看/proc/pid/maps文件来获悉某个具体进程的地址空间。但进程的地址空间并不对应实际的物理页,Linux采用Lazy的机制来分配实际的物理页("Demandpaging"和"和写时复制(CopyOnWrite)的技术"),从而提高实际内存的使用率。即每个虚拟内存页并不一定对应于物理页。虚拟页和物理页的对应是通过映射的机制来实现的,即通过页表进行映射到实际的物理页。因为每个进程都有自己的页表,因此可以保证不同进程从上到下(地址从高到低)依次为栈(函数内部局部变量),堆(动态分配内存),bss段(存未初始化的全局变量),数据段(存初始化的全局变量),文本段(存代码)同虚拟地址可以映射到不同的物理页,从而为不同的进程都可以同时拥有4G的虚拟地址空间提供了可能。

◆键盘敲一下到显示,电脑是怎么工作的

虚拟内存的管理:(三大方面)

给进程分配地址空间

交换

页和段的管理

linux内核对整个系统的物理内存是通过类型为structpage的数组mem_map来管理的。系统中的伙伴系统分配算法最终是通过操作这个数组来记录物理内存的分配、回收等操作。在这里不要被系统的高端内存、低端内存等概念搞混淆了,高、低端内存的分类主要在于区分物理内存地址是否可以直接映射到内核线性地址空间中。

从上面高、低端物理内存命名的由来我们可以知道,高、低端物理内存与具体的内存分配算法无关,它们都是被mem_map数组控制起来,再由伙伴分配系统实施管理。

为了把线性地址转化为物理地址,每个进程都有自己私有的页目录和页表。

linux在建立进程页目录时,把用户地址空间的页目录项(0~767项)清空而将内核页目录表(swapper_pg_dir)的第768项到1023项拷贝到进程的页目录表的第768项到1023项中。由于内核在初始化时也只映射了物理内存的前896M,我们可以知道内核目录表也只能保证第768项开始的224项中有有效映射。从这里我们可以知道,所有的进程都共享了其内核线性地址空间。

当一个进程在内核空间发生缺页故障的时候,这主要发生在访问内核空间动态映射区线性地址,在其处理程序中,就要通过0号进程的页目录(swapper_pg_dir)来同步本进程的内核页目录,实际上就是拷贝0号进程的内核页目录到本进程中(内核页表与进程0共享,故不需要复制)。如果进程0的该地址处的内核页目录也不存在,则出错,具体代码可以参考vmalloc的实现源码。

当进程运行于用户态时,若其需要申请内存空间,内核首先会在其用户线性空间中分配需要的线性地址空间,再通过伙伴分配系统分配物理内存并把分配的物理内存跟用户空间线性地址映射起来,最后再修改进程的页目录项及页表项写入这些映射关系。

◆CPU、内存和硬盘之间的关系

电脑是企业,内存是车间,CPU是生产线,硬盘是仓库,主板是地基,CPU速度快,生产就快,内存大,一次处理的原材料就多,所以提高机器速度有两条路,一是CPU升级,一是扩大内存,一次处理更多的信息产品,但CPU与内存又互相制约,车间再大,CPU慢也快不起来,CPU快,但车间小,一次送来的加工材料没多少,也快不了

◆几种地址和虚拟内存之间的关系(要会通过实例圆满解释)

逻辑地址(LogicalAddress)

例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(因为实模式没有分段或分页机制,Cpu不进行自动地址转换);逻辑也就是在Intel保护模式下程序执行代码段限长内的偏移地址(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地址打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程人员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。

线性地址(LinearAddress)(也就是虚拟地址)

是逻辑地址到物理地址变换之间的中间层。

程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。

1、分页单元中,页目录的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。

2、每一个进程,都有其独立的虚拟地址(线性地址)空间,运行一个进程,首先需要将它的页目录地址放到cr3寄存器中,将其他进程的保存下来。

3、每一个32位的线性地址被划分为三部分:页目录索引(10位):页表索引(10位):偏移(12位)

依据以下步骤进行地址转换:

1、装入进程的页目录地址(操作系统在调度进程时,把这个地址

装入CR3)

2、根据线性地址前十位,在页目录中,找到对应的索引项,页目

录中的项是一个页表的地址

3、根据线性地址的中间十位,在页表中找到页的起始地址

4、将页的起始地址与线性地址的最后12位相加,得到物理地址

物理地址(PhysicalAddress)

是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。

如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

虚拟内存(VirtualMemory)

是指计算机呈现出要比实际拥有的内存大得多的内存量。

因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个很恰当的比喻是:你不需要很长的轨道就可以让一列火车从上海开到北京。你只需要足够长的铁轨(比如说3公里)就可以完成这个任务。采取的方法是把后面的铁轨立刻铺到火车的前面,只要你的操作足够快并能满足要求,列车就能象在一条完整的轨道上运行。这也就是虚拟内存管理需要完成的任务。在Linux0.11内核中,给每个程序(进程)都划分了总容量为64MB的虚拟内存空间。因此程序的逻辑地址范围是0x0000000到0x4000000(即64M)。

与物理地址空间类似,线性地址空间也是平坦的4GB地址空间,地址范围从0到0xFFFFFFFF,线性空间中含有为系统定义的所有段和系统表。

有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的。

逻辑地址与物理地址的“差距”是0xC0000000,是由于虚拟地址->线性地址->物理地址映射正好差这个值。这个值是由操作系统指定的。

逻辑地址----(段表)--->线性地址—(页表)—>物理地址

◆linux的内存管理

Linux内核的设计并没有全部采用Intel所提供的段机制,仅仅是有限度地使用了分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC(精简指令集计算机(ReducedInstruction-SetComputer))处理器并不支持段机制。

由此可以得出,每个段的逻辑地址空间范围为0-4GB。因为每个段的基地址为0,因此,逻辑地址与线性地址保持一致(即逻辑地址的偏移量字段的值与线性地址的值总是相同的),在Linux中所提到的逻辑地址和线性地址(虚拟地址),可以认为是一致的。看来,Linux巧妙地把段机制给绕过去了,而完全利用了分页机制。

前面介绍了i386的二级页管理架构,不过有些CPU(Alpha64位)使用三级,甚至四级架构,Linux2.6.29内核为每种CPU提供统一的界面,采用了四级页管理架构,来兼容二级、三级、四级管理架构的CPU。

这四级分别为:

1.页全局目录(PageGlobalDirectory):即pgd,是多级页表的抽象最高层。

2.页上级目录(PageUpperDirectory):即pud。

3.页中间目录(PageMiddleDirectory):即pmd,是页表的中间层。

4.页表(PageTableEntry):即pte。

◆分布式操作系统:

◆单体内核与微内核体系结构

大内核:提供:调度,文件系统,网络,设备驱动,存储管理等。作为一个进程实现,所有的元素共享相同的地址空间

例如:Unix类系统,Linux

微内核:内核仅仅包含一些基本的功能:地址空间,进程间通信,基本的调度。其他的都由服务程序提供。

例如:Windows系统

◆操作系统的几大功能

1、程序开发

2、程序运行

3、I/O设备访问

4、文件访问控制

5、系统访问

6、错误检测与响应

7、审计

◆操作系统的特点

◆文件I/O操作部分:

C标准I/O库函数UnbufferedI/O函数

fopenopen

fdopencreat

fcloseclose

fseeklseek

freadread

fwritewrite

◆看看C标准I/O库函数是如何用系统调用实现的fopen()

调用open()打开指定的文件,返回一个文件描述符(就是一个int类型的编号),分配一个FILE结构体,其中包含该文件的描述符、I/O缓冲区和当前读写位置等信息,返回这个FILE结构体的地址。fgetc()

通过传入的FILE*参数找到该文件的描述符、I/O缓冲区和当前读写位置,判断能否从I/O缓冲区中读到下一个字符,如果能读到就直接返回该字符,否则调用read(),把文件描述符传进去,让内核读取该文件的数据到I/O缓冲区,然后返回下一个字符。注意,对于C标准I/O库来说,打开的文件由FILE*指针标识,而对于内核来说,打开的文件由文件描述符标识,文件描述符从open系统调用获得,在使用read、write、close系统调用时都需要传文件描述符。fputc()

判断该文件的I/O缓冲区是否有空间再存放一个字符,如果有空间则直接保存在I/O缓冲区中并返回,如果I/O缓冲区已满就调用write(2),让内核把I/O缓冲区的内容写回文件。fclose()

如果I/O缓冲区中还有数据没写回文件,就调用write()写回文件,然后调用close()关闭文件,释放FILE结构体和I/O缓冲区。

以写文件为例,C标准I/O库函数(printf()、putchar()、fputs())与系统调用write()的关系如下图所示。

图28.1.库函数与系统调用的层次关系

open、read、write、close等系统函数称为无缓冲I/O(UnbufferedI/O)函数,因为它们位于C标准库的I/O缓冲区的底层。用户程序在读写文件时既可以调用C标准I/O库函数,也可以直接调用底层的UnbufferedI/O函数,那么用哪一组函数好呢?

用UnbufferedI/O函数每次读写都要进内核,调一个系统调用比调一个用户空间的函数要慢很多,所以在用户空间开辟I/O缓冲区还是必要的,用C标准I/O库函数就比较方便,省去了自己管理I/O缓冲区的麻烦。

用C标准I/O库函数要时刻注意I/O缓冲区和实际文件有可能不一致,在必要时需调用fflush()。

在支持C语言的非UNIX操作系统上,标准I/O库的底层可能由另外一组系统函数支持,例如Windows系统的底层是Win32API,其中读写文件的系统函数是ReadFile、WriteFile。

图28.2.文件描述符表

至于已打开的文件在内核中用什么结构体表示,我们将在下一章详细介绍,目前我们在画图时用一个圈表示。用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引(即0、1、2、3这些数字),这些索引就称为文件描述符(FileDescriptor),用int型变量保存。当调用open打开一个文件或创建一个新文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符表项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read或write,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件。

我们知道,程序启动时会自动打开三个文件:标准输入、标准输出和标准错误输出。在C标准库中分别用FILE*指针stdin、stdout和stderr表示。这三个文件的描述符分别是0、1、2,保存在相应的FILE结构体中。头文件unistd.h中有如下的宏定义来表示这三个文件描述符:

#defineSTDIN_FILENO0

#defineSTDOUT_FILENO1

#defineSTDERR_FILENO2

事实上UnbufferedI/O这个名词是有些误导的,虽然write系统调用位于C标准库I/O底层,但在write的底层也可以分配一个内核I/O缓冲区,所以write也不一定是直接写到文件的,也可能写到内核I/O缓冲区中,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,而C标准库的I/O缓冲区则不具有这一特性(想一想为什么)。

◆知识小结:

1.C标准I/O库函数

1.1文件的创建,打开与关闭

原型为:

FILE*fopen(constchar*path,constchar*mode);

FILE*fdopen(intfd,constchar*mode);

intfclose(FILE*stream);

fopen以mode的方式打开或创建文件,如果成功,将返回一个文件指针,失败则返回NULL.fopen创建的文件的访问权限将以0666与当前的umask结合来确定。

mode的可选模式列表

模式读写位置截断原内容创建

rbYN文件头NN

r+bYY文件头NN

wbNY文件头YY

w+bYY文件头YY

abNY文件尾NY

a+bYY文件尾NY

在Linux系统中,mode里面的’b’(二进制)可以去掉,但是为了保持与其他系统的兼容性,建议不要去掉。

ab和a+b为追加模式,在此两种模式下,无论文件读写点定位到何处,在写数据时都将是在文件末尾添加,所以比较适合于多进程写同一个文件的情况下保证数据的完整性。

fdopen根据已经打开的文件描述符打开一文件指针,对同一个文件既打开文件描述符又打开文件指针,将容易出现问题,但是在多进程程序中,往往需要传递文件描述符,所以此类混合文件操作必须掌握。(不明白)

1.2读写文件

基于文件指针的数据读写函数较多,可分为如下几组:

数据块读写:

size_tfread(void*ptr,size_tsize,size_tnmemb,FILE*stream);

size_tfwrite(void*ptr,size_tsize,size_tnmemb,FILE*stream);

fread从文件流stream中读取nmemb个元素,写到ptr指向的内存中,每个元素的大小为size个字节.

fwrite从ptr指向的内存中读取nmemb个元素,写到文件中,每个元素size个字节。

所有的文件读写函数都从文件的当前读写点开始读写,读写完以后,当前读写点自动往后移动size*nmemb个字节。

1.3文件定位:

文件定位指读取或设置文件当前读写点,所有的通过文件指针读写数据的函数,都是从文件的当前读写点读写数据的。

常用的函数有:

intfseek(FILE*stream,longoffset,intwhence);

longftell(FILE*stream);

voidrewind(FILE*stream);

fseek设置当前读写点到offset处,

whence可以是SEEK_SET,SEEK_CUR,SEEK_END,这些值决定是

从文件头、当前点和文件尾计算偏移量offset.

ftell获取当前的读写点

rewind将文件当前读写点移动到文件头

1.4目录操作

获取目录信息:

#include

#include

DIR*opendir(constchar*name);//打开一个目录

structdirent*readdir(DIR*dir);//读取目录的一项信息,并返回该项信息的结构体指针

voidrewinddir(DIR*dir);//重新定位到目录文件的头部

intclosedir(DIR*dir);//关闭目录文件

读取目录信息的步骤为:

1>用opendir函数打开目录;

2>使用readdir函数迭代读取目录的内容,如果已经读取到目录末尾,又想重新开始读,则可以使用rewinddiw函数将文件指针重新定位到目录文件的起始位置;

3>用closedir函数关闭目录

1.5.标准输入/输出流

在进程一开始运行,就自动打开了三个对应设备的文件,它们是标准输入、输出、错误流,分别用全局文件指针stdin、stdout、stderr表示,stdin具有可读属性,缺省情况下是指从键盘的读取输入,stdout和stderr具有可写属性,缺省情况下是指向屏幕输入数据。

2、UnbufferedI/O函数

2.1.打开、创建和关闭文件

open和creat都能打开和创建函数,原型为

#include

#include

intopen(constchar*pathname,intflags);

intopen(constchar*pathname,intflags,mode_tmode);

intcreat(constchar*pathname,mode_tmode);

flags和mode都是一组掩码的合成值,flags表示打开或创建的方式,mode表示文件的访问权限。

flags的可选项有:

掩码含义

O_RDONLY以只读的方式打开

O_WRONLY以只写的方式打开

O_RDWR以读写的方式打开

O_CREAT如果文件不存在,则创建文件

O_EXCL仅与O_CREAT连用,如果文件已存在,则强制open失败

O_TRUNC如果文件存在,将文件的长度截至0

O_APPEND已追加的方式打开文件,每次调用write时,文件指针自动先移到文件尾,用于多进程写同一个文件的情况

O_NONBLOCK非阻塞方式打开

O_NODELAY非阻塞方式打开

O_SYNC只有在数据被真正写入物理设备设备后才返回

等价于

open(pathname,O_CREAT|O_TRUNC|O_WRONLY,mode);

文件使用完毕后,应该调用close关闭它,一旦调用close,则该进程对文件所加的锁全都被释放,并且使文件的打开引用计数减1,只有文件的打开引用计数变为0以后,文件才会被真正的关闭,文件引用计数主要用于多进程之间文件描述符的传递。

2.2.读写文件

读写文件的函数原型为:

#include

ssize_tread(intfd,void*buf,size_tcount);

ssize_twrite(intfd,constvoid*buf,size_tcount);

2.3.文件定位

函数lseek将文件指针设定到相对于whence,偏移值为offset的位置

off_tlseek(intfildes,off_toffset,intwhence);

whence可以是下面三个常量的一个

SEEK_SET从文件头开始计算

SEEK_CUR从当前指针开始计算

SEEK_END从文件尾开始计算

2.4.文件的锁定

在多进程对同一个文件进行读写访问时,为了保证数据的完整性,有时需要对文件进行锁定。可以通过fcntl对文件进行锁定和解锁。

intfcntl(intfd,intcmd,structflock*lock);

参数cmd置为F_GETLK或F_SETLK可以获取或设置文件锁定信息。

参数structflock为文件锁信息。

文件锁有两种类型,读取锁(共享锁)和写入锁(互斥锁)。对于已经加读取锁的文件,再加写入锁将会失败,但是允许其它进程继续加读取锁;对于已经加写入锁的文件,再加读取锁和写入锁都将会失败。

注意:文件锁只会对其它试图加锁的进程有效,对直接访问文件的进程无效。

2.5.文件描述符的复制

函数dup和dup2可以实现文件描述符的复制。原型为:

intdup(intoldfd);

intdup2(intoldfd,intnewfd);

文件描述符的复制是指用另外一个文件描述符指向同一个打开的文件,它完全不同于直接给文件描述符变量赋值,

例如:

描述符变量的直接赋值:

charszBuf[32];

intfd=open(“./a.txt”,O_RDONLY);

intfd2=fd;

close(fd);//导致文件立即关闭

printf(“read:%d\n”,read(fd2),szBuf,sizeof(szBuf)-1);

close(fd2);//读取失败,无意义了

在此情况下,两个文件描述符变量的值相同,指向同一个打开的文件,但是内核的文件打开引用计数还是为1,所以close(fd)或者close(fd2)都会导致文件立即关闭掉。

描述符的复制:

intfd2=dup(fd);

close(fd);//当前还不会导致文件被关闭,此时通过fd2照样可以访问文件

close(fd2);//内核的引用计数变为0,文件正式关闭

dup2(intfdold,intfdnew)也是进行描述符的复制,只不过采用此种复制,新的描述符由用户用参数fdnew显示指定,而不是象dup一样由内核帮你选定。对于dup2,如果fdnew已经指向一个已经打开的文件,内核会首先关闭掉fdnew所指向的原来的文件。如果成功dup2的返回值于fdnew相同,否则为-1.

2.6.标准输入输出文件描述符

与标准的输入输出流对应,在更底层的实现是用标准输入、标准输出、标准错误文件描述符表示的。它们分别用STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO三个宏表示,值分别是0、1、2三个整型数字。

我的小结:

在对两类文件操作的学习之后,常用的文件打开,创建,读写和关闭,

以及文件指针的定位,文件大小的求取等常用的操作都能掌握,在实际应用的过程出错不多,不过也有,曾经犯过fread读操作的错误,例如,在fread(void*ptr,size_tsize,size_tnmemb,FILE*stream)时;有时候一次读size(自定义一个较大的值)个字节,导致读失败,特别是在文件读取最后一次不足size个字节时,现在已经能正常应用,读的时候一个字节一个字节的读,写的时候一次写fread返回值那么多个字节,就能保证文件的读写不会出现两文件或文件和数组里面的内容的个数或大小不相同。

◆进程的基本元素(进程控制块)

栈堆

代码段

数据段

数据块

1、标志符(进程的PID)

2、状态(执行、就绪、阻塞、挂起)

3、优先级(在消息队列中)

4、程序计数器(配合PC寄存器)

5、内存指针(分配的空间)

6、上下文数据(context)

7、IO的状态信息(打开了哪些IO设备)

8、审计信息()

◆进程的轨迹

◆进程终止的原因(举例6个)

1、越界访问(数组)

2、除0溢出(inf)

3、无效指令(vim错写成vin)

4、数据误用(指针段错误)

5、父进程终止(终止所有子进程)

6、父进程请求(父进程有终止所有子进程的权利)

运行的在处理器中

◆未运行状态在队列中的排队

内存中

1处理器中只有一个程序在执行,而所有的就绪队列,阻塞队列都在内存中,而挂起状态在辅存中。

◆五状态模型

◆进程的控制结构

1、进程的位置

数据和程序保存需要内存

跟踪过程调用和过程间参数传递的栈

2、进程映像:

◆进程的创建

分配进程ID

唯一的

分配空间

初始化进程控制块

设置正确的连接?

创建和扩充其他的数据结构

◆进程切换的时机

时钟中断

IO中断

内存失效?

进程的地址是虚拟地址

包含这个虚拟地址的内存块调入主存中

Trap

发生错误或异常

进程被转换到退出状态

系统调用

比如打开文件

通常导致进程为阻塞状态

◆进程切换时,进程的状态变化

保存处理器上下文

PC和其他的寄存器

更新当前处于运行态的进程的进程控制块

进程的状态改变

把更新的进程的进程控制块移到相应的队列

选择另一个进程执行

更新所选择的进程控制块

进程的状态为运行态

更新内存管理的数据结构

管理地址转换

恢复新进程的处理器上下文

---------------------------------------华丽的分割线---------------------------------

***********************************************************

以下是linux子系统管理有关的知识

**********************************************************

◆进程的有关指令

查看进程ps–aux

查看当前进程和其父进程ps-e-opid,ppid,command

◆进程的四要素(必须掌握)

1.有一段程序供其执行。这段程序不一定是某个进程所专有,可以与其他进程共用。

2.有进程专用的内核空间堆栈。

3.在内核中有一个task_struct数据结构,即通常所说的“进程控制块”(PCB)。有了这个数据结构,进程才能成为内核调度的一个基本单位接受内核的调度(因为处理器处理进程时是需要进程的PID和状态信息等)。

4.有独立的用户空间。

◆进程的优点和缺点

优点:使多个程序并发执行

缺点:程序并发执行时付出了巨大的时空开销,每个进程在进行切换时身上带了过多的“累赘”导致系统效率降低。

◆linux进程描述

在Linux中,线程、进程都使用structtask_struct来表示,它包含了大量描述进程/线程的信息,其中比较重要的有:

■pid_tpid;

进程号,最大值10亿

■volatilelongstate/*进程状态*/(必须掌握)

1.TASK_RUNNING(运行态)

进程正在被CPU执行,或者已经准备就绪,随时可以执行。当一个进程刚被创建时,就处于TASK_RUNNING状态。

2.TASK_INTERRUPTIBLE(可中断)

处于等待中的进程,待等待条件为真时被唤醒,也可以被信号或者中断唤醒。

3.TASK_UNINTERRUPTIBLE(不可中断)

处于等待中的进程,待资源有效时唤醒,但不可以由其它进程通过信号(signal)或中断唤醒。

4.TASK_STOPPED(进程终止执行)

进程中止执行。当接收到SIGSTOP和SIGTSTP等信号时,进程进入该状态,接收到SIGCONT信号后,进程重新回到TASK_RUNNING。

5.TASK_KILLABLE

Linux2.6.25新引入的进程睡眠状态,原理类似于TASK_UNINTERRUPTIBLE,但是可以被致命信号(SIGKILL)唤醒。

6.TASK_TRACED

正处于被调试状态的进程。

7.TASK_DEAD

进程退出时(调用do_exit),state字段被设置为该状态。

■intexit_state/*进程退出时的状态*/

1、EXIT_ZOMBIE(僵死进程)

表示进程的执行被终止,但是父进程还没有发布waitpid()系统调用来收集有关死亡的进程的信息。

2、EXIT_DEAD(僵死撤销状态)

表示进程的最终状态。父进程已经使用wait()或waitpid()系统调用来收集了信息,因此进程将由系统删除。

◆linux进程的状态图

有上图可知,linux操作系统进程的状态模型和计算机操作系统的基本上是一样的,也是五状态模型。

◆Task_struct位置(必须掌握)

在2.4内核中是用1k的空间在栈的底部存放tast_struct,但随着运行的进程数量增加,task_struct大小增大并不断侵占栈空间,为了解决这个问题,在2.6内核中就改变成了上图的结构,把thread_infostructure替换原来的task_struct的位置,利用它的指针指向processDescriptor,在Linux中用current指针指向当前正在运行的进程的task_struct。

◆linux进程调度

■什么是调度?从就绪的进程中选出最适合的一个来执行。

■学习调度需要掌握哪些知识点?

1、调度策略

2、调度时机

3、调度步骤

■调度策略

1)SCHED_NORMAL(SCHED_OTHER):普通的分时进程

2)SCHED_FIFO:先入先出的实时进程

4)SCHED_BATCH:批处理进程

5)SCHED_IDLE:只在系统空闲时才能够被调度执行的进程

■调度类

调度类的引入增强了内核调度程序的可扩展性,这些类(调度程序模块)封装了调度策略,并将调度策略模块化。

1)CFS调度类(在kernel/sched_fair.c中实现)用于以下调度策略:SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE。

2)实时调度类(在kernel/sched_rt.c中实现)用于SCHED_RR和SCHED_FIFO策略。

■pick_next_task:选择下一个要运行的进程

◆调度时机

调度什么时候发生?即:schedule()函数什么时候被调用?

◆调度的发生有两种方式:

■主动式

在内核中直接调用schedule()。当进程需要等待资源等而暂时停止运行时,会把状态置于挂起(睡眠),并主动请求调度,让出CPU。

主动放弃cpu例:

1.current->state=TASK_INTERRUPTIBLE;

2.schedule();

■被动式(抢占)

用户抢占(Linux2.4、Linux2.6)

内核抢占(Linux2.6)

1、用户抢占

用户抢占发生在:

1)从系统调用返回用户空间。

2)从中断处理程序返回用户空间。

内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。

2、内核抢占

2)在支持内核抢占的系统中,更高优先级的进程/线程可以抢占正在内核空间运行的低优先级进程/线程。

■在支持内核抢占的系统中,某些特例下是不允许内核抢占的:

1)内核正进行中断处理。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。

2)内核正在进行中断上下文的BottomHalf(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。

3)进程正持有spinlock自旋锁、writelock/readlock读写锁等,当持有这些锁时,不应该被抢占,否则由于抢占将导致其他CPU长期不能获得锁而死等。

4)内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。

■为保证Linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_count,称为内核抢占计数。这一变量被设置在进程的thread_info结构中。每当内核要进入以上几种状态时,变量preempt_count就加1,指示内核不允许抢占。每当内核从以上几种状态退出时,变量preempt_count就减1,同时进行可抢占的判断与调度。比如说:中断嵌套,多层嵌套就会加几次,中断处理函数结束后就会逐个减一。

■内核抢占可能发生在:

1)中断处理程序完成,返回内核空间之前。

2)当内核代码再一次具有可抢占性的时候,如解锁及使能软中断等。

■调度标志TIF_NEED_RESCHED

作用:内核提供了一个need_resched标志来表明是否需要重新执行一次调度。

设置:

2)当一个优先级更高的进程进入可执行状态的时候,也会设

置这个标志。

◆调度步骤

Schedule函数工作流程如下:

1).清理当前运行中的进程;

2).选择下一个要运行的进程;

(pick_next_task分析)

3).设置新进程的运行环境;

4).进程上下文切换

◆信号定义

查看系统文件中的信号定义:vim/usr/include/bits/signum.h

◆进程和线程的区别和联系

进程和线程的区别在于:(这四点很重要,要熟练掌握)

1、进程是系统资源分配的最小单位,线程是系统调度的最小单位

2、进程有独立的内存单元,而线程是共享进程的堆栈,数据段,代码段(而进程只是共享代码段,且进程是分写时复制的功能(需要用时才共享堆栈数据))

3、线程必须依存于进程才能执行,应用程序提供多个线程执行控制。

4、进程执行过程中切换会浪费更多的资源,而线程则被称为轻量型进程。

1、线程的划分尺度小于进程,使得多线程程序的并发性高。

2、进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

3、线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

4、从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。

一般你运行一个应用程序,就生成了一个进程,这个进程拥有自己的内存空间,这个进程还可以内部生成多个线程,这些线程之间共用一个进程的内存空间,所以线程之间共享内存是很容易做到的,多线程协作比多进程协作快一些,而且安全。

◆C标准库函数创建进程

system("ls-l/home");

◆fork函数创建进程(通过复制当前进程来创建一个子进程)

1.成功:在父进程中返回子进程的PID,在子进程中返回0

2.失败:返回-1,子进程没有创建。

解析:fork()函数通过系统调用创建一个与原来进程(父进程)几乎完全相同的进程(子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。linux将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。),也就是这两个进程做完全相同的事。

◆exec族函数创建进程(负责读取可执行文件并将其载入地址空间)

char*argv[]={"ls","-l","/home/hou",NULL};

execl("/home/jincheng/test","test","aa","bb","cc",NULL,NULL);

exec("/bin/ls","ls","-l","/home/hou",NULL);

execvp("/bin/ls",argv);

execlp("ls","ls","-l","/home/hou",NULL);

解析:在fork后的子进程中使用exec函数族,可以装入和运行其它程序(子进程替换原有进程,和父进程做不同的事)。fork创建一个新的进程就产生了一个新的PID,exec启动一个新程序,替换原有的进程,因此这个新的被exec执行的进程的PID不会改变(和调用exec的进程的PID一样)。

◆父进程为什么要创建子进程

对于这种情况,子进程所体现出来的作用同函数和线程比较相似,可以看成是父进程在运行期间的一个过程,为此,需要由父进程来掌握子进程的启动、执行和退出。创建子进程才能多道程序并发执行,linux初始化的时候会创建swap进程、然后是init进程和一个init进程的兄弟进程,所有的进程(运行的程序)都是从父进程演化出去的,你可以看看proc里的东西,写个程序打印出各个进程的父进程~网上有源代码的,要的话我给你。

◆wait()、sleep()、exit()、abort()

pid_tpid;

pid=fork();

if(pid==0)

inti=20000;

sleep(5);

while(i--)

printf("%d\n",i);

printf("thisischildprocess\n");

elseif(pid>0)

wait();

printf("thisisparentprocess\n");

◆linux进程间为什么要通信

1)数据传输:

2)资源共享:

3)通知事件

4)进程控制

◆linux进程间通信(IPC)由以下几部分发展而来

1)UNIX进程间通信

2)基于SystemV进程间通信

3)POSIX(PortableOperatingSystemInterface)可移植操作系统接口

◆进程间通信方式概述:Linux系统中的进程通信方式主要以下几种:

同一主机上的进程通信方式

*UNIX进程间通信方式:包括管道(PIPE),有名管道(FIFO),和信号(Signal)

*SystemV进程通信方式:包括信号量(Semaphore),消息队列(MessageQueue),和共享内存(SharedMemory)

网络主机间的进程通信方式:

*RPC:RemoteProcedureCall远程过程调用

*Socket:当前最流行的网络通信方式,基于TCP/IP协议的通信方式.

◆几种进程间的通信方式的概念

#管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。

#有名管道(namedpipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信,可以自动同步,如管道写满后可以自动阻塞,管道的缓冲区是有限的(管道存在于内存中,在管道创建时,为缓冲区分配一个页面大小);管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

#信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间(如:父子进程)以及同一进程内不同线程之间的同步手段。

#消息队列(messagequeue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。

#信号(sinal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

#共享内存(sharedmemory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC(InternetProcessConnection)(进程间通信方式)方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制配合使用,如信号量,来实现进程间的同步和通信。

#套接字(socket):套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同进程间的进程通信。

◆互斥和同步的关系

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

◆进程间的数据通信方式和特点

由于不同的进程运行在各自不同的内存空间中.一方对于变量的修改另一方是无法感知的.因此.进程之间的信息传递不可能通过变量或其它数据结构直接进行,只能通过进程间通信来完成。

根据进程通信时信息量大小的不同,可以将进程通信划分为两大类型:控制信息的通信和大批数据信息的通信.前者称为低级通信,后者称为高级通信。

低级通信主要用于进程之间的同步、互斥、终止、挂起等等控制信息的传递。

高级通信主要用于进程间数据块的交换和共享,常见的高级通信有管道(PIPE)、消息队列(MESSAGE)、共享内存(SHAREDMEM0RY)等。

这里主要比较一下高级通信的这三种方式的特点。

管道通信(PIPE)两个进程利用管道进行通信时.发送信息的进程称为写进程.接收信息的进程称为读进程。管道通信方式的中间介质就是文件.通常称这种文件为管道文件.它就像管道一样将一个写进程和一个读进程连接在一起,实现两个进程之间的通信。写进程通过写入端(发送端)往管道文件中写入信息;读进程通过读出端(接收端)从管道文件中读取信息。两个进程协调不断地进行写和读,便会构成双方通过管道传递信息的流水线。利用系统调用PIPE()可以创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用MKNOD()可以创建一个有名管道文件.通常称为有名管道或FIFO。无名管道是一种非永久性的管道通信机构.当它访问的进程全部终止时,它也将随之被撤消。无名管道只能用在具有家族联系的进程之间。有名管道可以长期存在于系统之中.而且提供给任意关系的进程使用,但是使用不当容易导致出错.所以操作系统将命名管道的管理权交由系统来加以控制管道文件被创建后,可以通过系统调用WRITE()和READ()来实现对管道的读写操作;通信完后,可用CLOSE()将管道文件关闭。

消息缓冲通信(MESSAGE)多个独立的进程之间可以通过消息缓冲机制来相互通信.这种通信的实现是以消息缓冲区为中间介质.通信双方的发送和接收操作均以消息为单位。在存储器中,消息缓冲区被组织成队列,通常称之为消息队列。消息队列一旦创建后即可由多进程共享.发送消息的进程可以在任意时刻发送任意个消息到指定的消息队列上,并检查是否有接收进程在等待它所发送的消息。若有则唤醒它:而接收消息的进程可以在需要消息的时候到指定的消息队列上获取消息.如果消息还没有到来.则转入睡眠状态等待。

共享内存通信(SHAREDMEMORY)针对消息缓冲需要占用CPU进行消息复制的缺点.OS提供了一种进程间直接进行数据交换的通信方式一共享内存顾名思义.这种通信方式允许多个进程在外部通信协议或同步,互斥机制的支持下使用同一个内存段(作为中间介质)进行通信.它是一种最有效的数据通信方式,其特点是没有中间环节.直接将共享的内存页面通过附接.映射到相互通信的进程各自的虚拟地址空间中.从而使多个进程可以直接访问同一个物理内存页面.如同访问自己的私有空间一样(但实质上不是私有的而是共享的)。因此这种进程间通信方式是在同一个计算机系统中的诸进程间实现通信的最快捷的方法.而它的局限性也在于此.即共享内存的诸进程必须共处同一个计算机系统.有物理内存可以共享才行。

三种方式的特点(优缺点):

1、无名管道简单方便.但局限于单向通信的工作方式.并且只能在创建它的进程及其子孙进程之间实现管道的共享:有名管道虽然可以提供给任意关系的进程使用.但是由于其长期存在于系统之中,使用不当容易出错。

3、共享内存针对消息缓冲的缺点改而利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。但是共享内存的通信方式是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的.因此,这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其他同步工具解决。另外,由于内存实体存在于计算机系统中.所以只能由处于同一个计算机系统中的诸进程共享,不方便网络通信。

进程间通信方式剖析

◆无名管道(PIPE)

管道是UNIX系统IPC的最古老形式,所有的UNIX系统都支持这种通信机制。有两个局限性:

(1)支持半双工;

(2)只有具有亲缘关系(即父子进程间)的进程之间才能使用这种无名管道;

例程:

#include

#include

intfds[2];

intstatus;

if(pipe(fds)==-1)

printf("createpipefailure!\n");

sleep(2);

charstr[100];

memset(str,0,100);

//printf("%s\n",str);

close(fds[1]);

if((read(fds[0],str,100))!=-1)

printf("read=%s\n",str);

close(fds[0]);

charpstr[100]="Helloeveryone";

if((write(fds[1],pstr,100))!=-1)

printf("writeHelloeveryonesuccess!\n");

sleep(3);

函数原型:#include

intpipe(intfiledes[2]);

功能:创建无名管道

参数:经由参数filedes返回两个文件描述符,filedes[0]为读而打开,filedes[1]为写而打开。

返回值:返回0,创建成功;返回1,创建失败。

使用管道的注意事项:

1.当读一个写端已经关闭的管道时,在所有数据被读取之后,read函数返回值为0,以指示到了文件结束处;

2.如果写一个读端关闭的管道,则产生SIGPIPE信号。如果忽略该信号或者捕捉该信号并处理程序返回,则write返回-1,errno设置为EPIPE

◆有名管道(FIFO)

又称命名管道,可用于两个任意进程间的通信,它相当于一个文件。

创建函数原型:

intmkfifo(constchar*pathname,mode_tmode)

功能:创建有名管道

参数:

1)pathname:普通的路径名,就是创建后FIFO的文件名

2)mode:属性:创建的这个FIFO,一般的文件访问函数(open、close、read、write)都可用于FIFO;比如:O_CREAT|O_EXCL,O_EXCL表示的是:如果使用O_CREAT时文件存在,就返回错误信息,提示你更换名字,防止文件创建时的重复。如:

如果文件事先已经存在,open(pathname,O_RDWR|O_CREAT,0666);打开成功,返回一个大于0的fdopen(pathname,O_RDWR|O_CREAT|O_EXCL,0666);打开失败,返回-1

3)非阻塞标志O_NONBLOCK:没有时,访问要求无法满足时将阻塞;使用时,访问要求无法满足时不阻塞,立刻出错返回,errno是ENXIO。

在内核中是,检测其标志!如果存在O_NONBLOCK,读写操作将会立即返回,否则内核通过调度其它进程阻塞当前进程!当目的事件发生时内核会唤醒它!

对于一个给定的描述符两种方法对其指定非阻塞I/O:(1)调用open获得描述符,并指定O_NONBLOCK标志(2)对已经打开的文件描述符,调用fcntl,打开O_NONBLOCK文件状态标志。intflags,s为描述符

flags=fcntl(s,F_GETFL,0))

fcntl(s,F_SETFL,flags|O_NONBLOCK)

4)返回值:如果路径已存在,返回EEXIST错误;如果确实返回该错

误,直接打开即可。

1)读程序

#include

#defineFIFO_R"/home/xitongbiancheng/jinchengjiantongxin/pipe/myfifo"

intmain(intargc,char**argv)

intfd,rd;

charbuf[100];

if((mkfifo(FIFO_R,O_CREAT|O_EXCL)<0)&&errno!=EEXIST)

printf("cannotcreatmyfifo!\n");

printf("preparereadingthepipe...\n");

if((fd=open(FIFO_R,O_RDONLY|O_NONBLOCK,0))==-1)

printf("openerror!\n");

exit(1);

while(1)

memset(buf,0,sizeof(buf));

if((rd=read(fd,buf,100))==-1)

if(errno==EAGAIN)

printf("nodatayet\n");

printf("thedatafromreadingis:%s\n",buf);

sleep(1);

pause();

2)写程序

#defineFIFO_W"/home/xitongbiancheng/jinchengjiantongxin/pipe/myfifo"

if(argc==1)

printf("Pleaseentertheargv!\n");

exit(0);

strcpy(buf,argv[1]);

if((fd=open(FIFO_W,O_WRONLY|O_NONBLOCK,0))==-1)

if((rd=write(fd,buf,sizeof(buf)))==-1)

printf("theFIFOhasnotready,pleasetryitagain!\n");

printf("writesuccess,thestringis%s\n",buf);

【注意调试】看看O_NONBLOCK有无的作用是什么?

◆信号(signal)

信号是Unix系统中最为古老的进程间通信机制之一,如按键按下,除0溢出,非法访问,kill函数,kill命令都能产生信号。Linux中有30种信号。

信号类型:(常见的信号如下:)

DIGHUP:从终端发出的结束信号

SIGINT:来自键盘中断信号

SIGKILL:该信号结束接收信号的进程

SIGTERM:kill命令发出的信号

SIGCHLD:标志子进程停止或结束的标志

SIGSTOP:来自键盘(CTRL-Z)或调试程序的停止执行信号

信号处理:

1)忽略信号(SIGKILL、SIGSTOP除外)

2)执行用户希望的动作(通知内核在某种信号发生时,调用一个用户函数,在用户函数中,执行用户希望的处理),signal函数就实现了这个功能

#include

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

怎么理解这个函数:

typedefvoid(*sighandler_t)(int)

sighandler_tsignal(intsignum,sighandler_thandler)

fun可能的值:

SIGQUITE:退出程序值为3

SIGINT:值为2

SIG_IGN:忽略此信号值为1

SIG_DFL:按系统默认方式处理值为0

信号处理函数名:使用该函数处理

3)执行系统默认动作(一般是终止进程)

信号发送:kill函数、raise函数、alarm函数#include

intkill(pid_tpid,intsigno)

intraise(intsigno)

4)wait()和pause()的区别

wait():结束等待的条件是子进程退出

pause():结束等待的条件是收到一个信号

voidmy_func(intsign_no)

if(sign_no==SIGINT)

printf("IhavegetSIGINT\n");

if(sign_no==SIGQUIT)

printf("IhavegetSIGQUIT\n");

printf("waitingforsignalSIGINTorSIGQUIT\n");

signal(SIGINT,my_func);

signal(SIGQUIT,my_func);

执行结果:waitingforsignalSIGINTorSIGQUIT

紧接着,再打开一个终端,kill-sSIGQUIT6430,6430是这个进程的进程号(每一次都需要查找进程号ps–aux),这样是将一个进程杀死,等待程序结果是:IhavegetSIGQUIT

◆消息队列(messagequeue)

特点:

1.消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.2.消息队列允许一个或多个进程向它写入与读取消息.3.管道和命名管道通信数据都是先进先出的原则。4.消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。

5.消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

类型:1)POSICX(可移植的操作系统接口)消息队列

2)系统V消息队列

持续性:系统V消息队列是随内核持续的

删除方式:1)内核重启2)人工删除

键值:每个消息队列对应一个唯一的键值

1)获取键值key

#include

key_tftok(char*pathname,charproj)

参数:pathname:文件名

proj:项目名(不为0即可)

返回值:返回文件名对应的键值,失败返回-1

第一个参数不用解释都知道是一个文件路径吧,第二个参数的最后8位(只有后八位有效,0-255)与第一个参数一起确定一个key.(常用于进程)。比如:我们在开发一个项目的时候,有可能不同人需要在同一个路径下编写代码,防止大家不小心使用了相同的key,一般项目经理会分配给每个人不同的proj_t,这个时候就可以用当前路径pathname和proj_t生成所需的key,也就是说:同一个proj_t不可以出现在同一个路径。

ABCD四个进程,其中AB希望访问相同的shm,CD希望访问相同的shm,那么就可以商定A/B使用文件名/path/to/xxx来计算shmkey,而C/D使用/path/to/yyy来计算,这样就可以达到A/BC/D各自使用正确的资源,而互不干扰。

ftok不会操作文件本身的,他只是根据文件名做计算算出一个唯一的key(文件名不同,key不同)而已。

问:这个路径上的文件到底起到什么样的作用?程序执行后这个文件中有什么内容?

2)创建消息队列

#include

intmsgget(key_tkey,intmsgflg)

参数:key:由ftok获得

msgflg:标志位

IPC_CREAT:创建新的消息队列

IPC_EXEC:若消息队列已存在,则返回错误

IPC_NOWAIT:读写消息队列要求无法满足时,不阻塞

返回值:与key值相对应的消息队列描述字,创建失败则返回-1

注意:创建消息队列的两种情况:

1、如果没有与键值key相对应的消息队列,且msgflg中包含了IPC_CREAT标志位。

2、key参数为IPC_PRIVATE

3)发送消息(写成函数)

intmsgsnd(intmsqid,structmsgbuf*msgp,intmsgsz,intmsgflg)

参数:msqid:已打开消息队列id,这个id是通过getpid()得到的。

msgp:存放消息队列的结构体

structmsgbuf{

longmtype;//消息类型用0,1,2等来标记

charmtext[1];

}//消息数据的首地址

msgsz:消息队列的长度

msgflg:发送标志:IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待,这里是不阻塞,也就是说有空间就写,没有空间就冲过去,回头再说。

4)接收消息(写成函数)

intmsgrcv(intmsqid,structmsgbuf*msgp,intmsgsz,

longmsgtyp,intmsgflg)

参数:msgtyp:消息类型

msgp:消息取出后,存储在msgp指向的msgbuf中,成功的读取后,队列中的这条消息被删除,就像队列一样,取出去的就没有了。

structmsg_buf

longmtype;

charmtext[50];

};

main()

key_tkey;

key=ftok("/home/xitongbiancheng/jinchengjiantongxin/message_queue/1.dat",'a');

if(key==-1)

printf("filechangefail!\n");

printf("key=[%x]\n",key);

intmes_c;

mes_c=msgget(key,IPC_CREAT);

if(mes_c==-1)

printf("messagequeuecreatfail!\n");

intmes_s,megsz;

structmsg_bufmesbuf;

mesbuf.mtype=getpid();

megsz=sizeof(mesbuf)-sizeof(mesbuf.mtype);

strcpy(mesbuf.mtext,"1234567890");

mes_s=msgsnd(mes_c,&mesbuf,megsz,IPC_NOWAIT);

if(mes_s==-1)

printf("sendmessageerror!\n");

intmes_r;

memset(&mesbuf,0,sizeof(mesbuf));

mes_r=msgrcv(mes_c,&mesbuf,megsz,mesbuf.mtype,IPC_NOWAIT);

strcat(mesbuf.mtext,"houyunliang");

if(mes_r==-1)

printf("readmessageerror!\n");

printf("themessagefromreadingis:%s\n",mesbuf.mtext);

◆信号量

概述:

信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:(1)测试控制该资源的信号量。(2)若此信号量的值为正,则允许进行使用该资源,进程将信号量减1。(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。(4)当进程不再使用一个信号量控制的资源时,信号量值加1;如果此时有进程正在睡眠等待此信号量,则唤醒此进程。维护信号量状态的是Linux内核操作系统而不是用户进程。

分类:1)二值信号量(只能两个进程访问)

2)计数信号量(可多个进程访问)

信号量和互斥锁的区别:

信号量:是进程间(线程间)同步用的,一个进程(线程)完成了某一个动作就通过信号量告诉别的进程(线程),别的进程(线程)再进行某些动作。

互斥锁:是线程间互斥用的,一个线程占用了某一个共享资源,那么别的线程就无法访问,直到这个线程离开,其他的线程才开始可以使用这个共享资源。可以把互斥锁看成二值信号量。

上锁时:

信号量:只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一。一句话,信号量的value>=0。

互斥锁:只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞,等待资源可用。一句话,线程互斥锁的vlaue可以为负数,互斥体用于保护共享的易变代码,也就是,全局或静态数据,这样的数据必须通过互斥体进行保护,以防止它们在多个线程同时访问时损坏。

使用场所:

信号量主要适用于进程间通信,当然,也可用于线程间通信。而互斥锁只能用于线程间通信。

#include

#defineMAX3

unionsemun

intval;

structsemid_ds*buf;

unsignedshortint*array;

structseminfo*_buf;

intsig_alloc(key_tkey,intsem_flags)

returnsemget(key,MAX,sem_flags);//key=0;MAX=3;sem_flags=512

intsig_destory(intsemid,intnumth)

unionsemunignored_argument;

returnsemctl(semid,numth,IPC_RMID,ignored_argument);

intsig_init(intsemid,unsignedshortint*parray)

unionsemunargument;

inti=0;

argument.array=parray;

for(i=0;i

semctl(semid,i,SETALL,argument);//SETALL:17

intsig_wait(intsemid,intnumth)

structsembufoperations[MAX];

operations[numth-1].sem_num=numth-1;

operations[numth-1].sem_op=-1;

operations[numth-1].sem_flg=SEM_UNDO;//SEG_UNDO=4096

returnsemop(semid,&operations[numth-1],1);

intsig_post(intsemid,intnumth)

operations[numth-1].sem_op=1;

operations[numth-1].sem_flg=SEM_UNDO;

intsig_id,i=0;

unsignedshortintsig_val[MAX]={1,1,1};

sig_id=sig_alloc(0,IPC_CREAT);//IPC_CREAT=512

sig_init(sig_id,sig_val);

while(++i<10)

sig_wait(sig_id,3);

printf("***************\n");

sig_post(sig_id,3);

elseif(pid)

i=0;

sig_wait(sig_id,1);

printf("+++++++++++++++\n");

sig_post(sig_id,1);

return1;

(1)intsemget(key_tkey,intnum_sems,intsem_flags);

semget函数创建一个新的信号量或获得一个已存在的信号量键值。

数值。所有的信号量是为不同的程序通过提供一个key来间接访问

的,对于每一个信号量系统生成一个信号量标识符。信号量键值只可

以由semget获得,所有其他的信号量函数所用的信号量标识符都是

由semget所返回的。

还有一个特殊的信号量key值,IPC_PRIVATE(通常为0),其作用是

创建一个只有创建进程可以访问的信号量。

num_sems:是所需要的信号量数目。这个值通常总是1。

sem_flags:是一个标记集合,与open函数的标记十分类似。低

九位是信号的权限,其作用与文件权限类似。另外,这些标记可以与

IPC_CREAT进行或操作来创建新的信号量。设置IPC_CREAT标记并且

指定一个已经存在的信号量键值并不是一个错误。如果不需要,

IPC_CREAT标记只是被简单的忽略。我们可以使用IPC_CREAT与

IPC_EXCL的组合来保证我们可以获得一个新的,唯一的信号量。如

果这个信号量已经存在,则会返回一个错误。

如果成功,semget函数会返回一个正数;这是用于其他信号量函数

的标识符。如果失败,则会返回-1。

(2)intsemop(intsem_id,structsembuf*sem_ops,size_tnum_sem_ops);

函数semop用来改变信号量的值

sem_id:是由semget函数所返回的信号量标识符。

sem_ops:是一个指向结构数组的指针,其中的每一个结构至少包含下列成员:

structsembuf

shortsem_num;

shortsem_op;

shortsem_flg;

sem_num:是信号量数目,通常为0,除非我们正在使用一个信号量数组。

sem_op:是信号量的变化量值。(我们可以以任何量改变信号量值,而不只是1)通常情况下中使用两个值,-1是我们的P操作,用来等待一个信号量变得可用,而+1是我们的V操作,用来通知一个信号量可用。

sem_flg:通常设置为SEM_UNDO。这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没有释放这个信号量,如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。将sem_flg设置为SEM_UNDO是一个好习惯,除非我们需要不同的行为。如果我们确实我们需要一个不同的值而不是SEM_UNDO,一致性是十分重要的,否则我们就会变得十分迷惑,当我们的进程退出时,内核是否会尝试清理我们的信号量。semop的所用动作会同时作用,从而避免多个信号量的使用所引起的竞争条件。

(3)intsemctl(intsem_id,intsem_num,intcommand,...);

semctl函数允许信号量信息的直接控制。

sem_id,是由semget所获得的信号量标识符。sem_num参数是信号量数目。当我们使用信号量数组时会用到这个参数。通常,如果这是第一个且是唯一的一个信号量,这个值为0。

command:参数是要执行的动作,而如果提供了额外的参数,则是unionsemun,根据X/OPEN规范,这个参数至少包括下列参数:

unsignedshort*array;

许多版本的Linux在头文件(通常为sem.h)中定义了semun联合,尽管X/Open确认说我们必须定义我们自己的联合。如果我们发现我们确实需要定义我们自己的联合,我们可以查看semctl手册页了解定义。如果有这样的情况,建议使用手册页中提供的定义,尽管这个定义与上面的有区别。有多个不同的command值可以用于semctl。在这里我们描述两个会经常用到的值。要了解semctl功能的详细信息,我们应该查看手册页。

这两个通常的command值为:

SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。

IPC_RMID:当信号量不再需要时用于删除一个信号量标识。

semctl函数依据command参数会返回不同的值。对于SETVAL与IPC_RMID,如果成功则会返回0,否则会返回-1。

◆共享内存

共享内存是运行在同一台机器上的进程间通信最快的方式,因为数据不需要在不同的进程间复制。通常由一个进程创建一块共享内存区,其余进程对这块内存区进行读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的将是实际的物理内存,在Linux系统下,这只有通过限制Linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmXXX函数族来实现利用共享内存进行存储的。

使用共享内存时要掌握的唯一诀窍是多个进程之间对一定存储区的同步访问。若服务器进程正在将数据放入共享内存,则在它做完这一操作之前,客户进程不应当去读取这些数据,也就是说先写后读。

通常,信号量是用来实现对共享内存访问的同步(记录锁也可以用于这种场合)。

例程1:

#include

{pid_tpid;

intshare_id;

share_id=shmget(IPC_PRIVATE,getpagesize(),IPC_CREAT|S_IRUSR|S_IWUSR|IPC_EXCL);

char*share1=NULL;

share1=(char*)shmat(share_id,0,0);

memset(share1,0,getpagesize());

strcpy(share1,"helloworld\n");

//printf("%s\n",share1);

shmdt(share1);

char*share2=NULL;

share2=(char*)shmat(share_id,0,0);

printf("readcharactersfromsharedmemory!%p\n",share2);

//printf("%s",share2);

shmctl(share_id,IPC_RMID,0);

例程2:

#defineFLAGIPC_CREAT|S_IRUSR|S_IWUSR|IPC_EXCL

main(intargc,char**argv)

if(argc!=2)

printf("theparameterisnotenough,pleaseenteragain!\n");

share_id=shmget(IPC_PRIVATE,1024,FLAG);

if(share_id==-1)

printf("sharingRAMisfail\n");

memset((void*)share1,'\0',1024);

strncpy(share1,argv[1],1024);

strcat(share1,"67890");

printf("theRAMfromsharingis:%s\n",share2);

shmdt(share2);

(1)内核为每个共享存储段设置了一个shmid_ds结构。

(2)分配阶段:若要获得一个共享存储标识符,调用的第一个函数通常是shmget。

intshmget(key_tkey,size_tsize,intflag);

key_tkey:表示共享内存的键值一般是系统宏定义IPC_PRIVATE(值为0)即创建一块新的共享内存

参数size:该共享存储段的长度(1024、2048等)(单位:字节)一般是一页的大小,(4096)即得到4K的共享存储单元。

返回值:若成功则返回共享内存标志符ID,若出错则返回-1

(3)绑定阶段:一旦创建了共享存储段,进程就可调用shmat函数将其连接到相应进程的地址空间中。

voidshmat(intshmid,constvoid*addr,intflag);

shmid:为shmget函数返回的共享内存标志符

addr:参数决定了以什么方式来确定连接的地址,这就是共享内存的地址,如果是NULL或0,则系统会自动分配合适的地址空间。

返回值:若成功则返回共享存储的指针,即使该进程数据段所连接的实际地址,进程可对此内存地址进行读写操作;若出错则返回-1

(4)管理阶段

man数据:shmdt()detachesthesharedmemorysegmentlocatedattheaddressspecifiedbyshmaddrfromtheaddressspaceofthecallingprocess.Theto-be-detachedsegmentmustbecurrentlyattachedwithshmaddrequaltothevaluereturnedbytheattachingshmat()call.

(5)删除阶段

shmctl函数对共享存储段执行多种操作。你应当在结束使用每个共享内存块的时候都使用shmctl进行释放,以防止超过系统所允许的共享内存块的总数限制

intshmctl(intshmid,intcmd,structshmid_ds*buf);

intshmid:为shmget函数返回的共享内存标志符

intcmd:这是删除或其他操作时所用的命令,如:

IPC_RMID(值为0)man数据:Markthesegmenttobedestroyed.

IPC_STAT(值为2)man数据:Copyinformationfromthekerneldatastructureassociatedwithshmidintotheshmid_dsstructurepointedtobybuf.

structshmid_ds*buf:buf是个结构体指针指向一个复杂的结构体。

返回值:若成功则返回0,若出错则返回-1

◆内存映射

Linux提供了内存映射函数mmap,它把文件内容映射到一段内存上(准确说是虚拟内存上),通过对这段内存的读取和修改,实现对文件的读取和修改。

程序:

#defineFILE_LENGTH1024

intmain(intargc,char*argv[])

intfd1,fd2;char*pfile=NULL;

char*load=NULL;

intnum;

if(argc<3)

printf("pleaseinputmorefile\n");

fd1=open(argv[1],O_RDONLY);

fd2=open(argv[2],O_RDWR|O_CREAT,S_IRUSR|S_IWUSR);

printf("fd2=%d\n",fd2);

//fd2=open(argv[2],O_WRONLY);

lseek(fd2,FILE_LENGTH+1,SEEK_SET);

write(fd2,"",1);

lseek(fd2,0,SEEK_SET);

load=malloc(FILE_LENGTH);

if(load==NULL)

printf("mallocfailed\n");

num=read(fd1,load,FILE_LENGTH);

printf("num=%d\n",num);

pfile=mmap(0,1024,PROT_WRITE|PROT_READ,MAP_PRIVATE,fd2,0);

close(fd2);

printf("pfile=%d\n",pfile);

memcpy(pfile,load,FILE_LENGTH);

printf("qqqq\n");

munmap(pfile,FILE_LENGTH);

close(fd1);

free(load);

(1)头文件:

原型:void*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffsize);

例程:pfile=mmap(0,1024,PROT_WRITE|PROT_READ,MAP_PRIVATE,fd2,0);

返回值:成功则返回映射区起始地址,失败则返回MAP_FAILED(-1).

参数:

addr:指定映射的起始地址,通常设为NULL,由系统指定.

length:将文件的多大长度映射到内存.

prot:映射区的保护方式,可以是:

PROT_EXEC:映射区可被执行.

PROT_READ:映射区可被读取.

PROT_WRITE:映射区可被写入.

PROT_NONE:映射区不能存取.

flags:映射区的特性,可以是:

MAP_SHARED:对映射区域的写入数据会复制回文件,且允许其他映射该文件的进程共享.

MAP_PRIVATE:对映射区域的写入操作会产生一个映射的复制(copy-on-write),对此区域所做的修改不会写回原文件.

fd:由open返回的文件描述符,代表要映射的文件.

offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为0,表示从文件头开始映射.

(2)intopen(constchar*pathname,intflags);

返回值:文件的文件描述符

参数:constchar*pathname:文件名

intflags:读写方式

(3)内存映射的步骤:

注意事项:

在修改映射的文件时,只能在原长度上修改,不能增加文件长度,因为内存是已经分配好的.

open和fopen的区别:

1.缓冲文件系统

缓冲文件系统的特点是:在内存开辟一个“缓冲区”,为程序中的每一个文件使用,当执行读文件的操作时,从磁盘文件将数据先读入内存“缓冲区”,装满后再从内存“缓冲区”依此读入接收的变量。执行写文件的操作时,先将数据写入内存“缓冲区”,待内存“缓冲区”装满后再写入文件。由此可以看出,内存“缓冲区”的大小,影响着实际操作外存的次数,内存“缓冲区”越大,则操作外存的次数就少,执行速度就快、效率高。一般来说,文件“缓冲区”的大小随机器而定。

fopen,fclose,fread,fwrite,fgetc,fgets,fputc,fputs,freopen,fseek,ftell,rewind等

2.非缓冲文件系统

缓冲文件系统是借助文件结构体指针来对文件进行管理,通过文件指针来对文件进行访问,既可以读写字符、字符串、格式化数据,也可以读写二进制数据。非缓冲文件系统依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不设文件结构体指针,只能读写二进制文件,但效率高、速度快,由于ANSI标准不再包括非缓冲文件系统,因此建议大家最好不要选择它。本书只作简单介绍。open,close,read,write,getc,getchar,putc,putchar等。

open是系统调用返回的是文件句柄,文件的句柄是文件在文件描述副表里的索引,fopen是C的库函数,返回的是一个指向文件结构的指针。

fopen是ANSIC标准中的C语言库函数,在不同的系统中应该调用不同的内核api

linux中的系统函数是open,fopen是其封装函数,个人观点。仅供参考。

文件描述符是linux下的一个概念,linux下的一切设备都是以文件的形式操作.如网络套接字、硬件设备等。当然包括操作文件。

fopen是标准c函数。返回文件流而不是linux下文件句柄。

设备文件不可以当成流式文件来用,只能用open

fopen是用来操纵正规文件的,并且设有缓冲的,跟open还是有一些区别

一般用fopen打开普通文件,用open打开设备文件

fopen是标准c里的,而open是linux的系统调用.

他们的层次不同.

fopen可移植,open不能

我认为fopen和open最主要的区别是fopen在用户态下就有了缓存,在进行read和write的时候减少了用户态和内核态的切换,而open则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列快;如果随机访问文件open要比fopen快。

来自论坛的经典回答:

前者属于低级IO,后者是高级IO。

前者返回一个文件描述符(用户程序区的),后者返回一个文件指针。

前者无缓冲,后者有缓冲。

前者与read,write等配合使用,后者与fread,fwrite等配合使用。

后者是在前者的基础上扩充而来的,在大多数情况下,用后者。

内存映射文件(mmap)

内存映射文件是利用虚拟内存把文件映射到进程的地址空间中去,在此之后进程操作文件,就像操作进程空间里的地址一样了,比如使用memcpy等内存操作的函数。这种方法能

够很好的应用在需要频繁处理一个文件或者是一个大文件的场合,这种方式处理IO效率比

普通IO效率要高。另外,UNIX把它做为内存共享来设计的。

1、创建一个内存映射区域

void*mmap(void*addr,size_tlen,intprot,intflag,intfiledes,off_toff);

addr

映射区首地址,你想自己定义的时候使用。一般使用NULL,然后系统自动分配一个合适地址。

len

映射的长度,单位byte

prot

说明映射区访问属性:读、写、执行、不可访问

可PROT_READ,PROT_WRITE,PROT_EXEC,PROT_NONE

不能超越它所映射的文件的权限

flag

MAP_SHARED这个标志说明文件映射是共享的,也就是说进程改变了内存映射,也就会影响到文件。

MAP_PRIVATE这个标志说明文件映射不共享,打开文件映射的进程只能改变的是这个文件的一个副本。

filedes

文件描述符号

off

隐射位置的偏移量,设置为0的话,就映射文件的0-len个字节

返回

映射区域的首地址

2、取消文件映射

intmunmap(caddr_taddr,size_tlen);

内存隐射的地址。mmap返回的地址。

隐射的字节数。

成果0,失败负

使用MAP_PRIVATE的映射改变将不被写回文件。

3、内存映射和文件的同步

intmsync(void*addr,size_tlen,intflags);

内存映射地址

长度

flags

MS_ASYNC,MS_SYNC,MS_INVALIDATE。

MS_ASYNC,异步写,调用后就返回不等待写完,MS_SYNC则等待写完才返回。

MS_INVALIDATE,写完之后,内存映射中与文件不同的数据将无效,取而代之的是文件中的数据。

成功0,失败负

4、创建共享内存区

这个信号量比较相同。

intshm_open(constchar*name,intoflag,mode_tmode);

对比:

sem_t*sem_open(constchar*name,intoflag,/*mode_tmode,unsignedintvalue*/);

这个地方就可以解释sem_open函数的文件名有什么用了。使用shem_open创建共享文件,

使用mmap使内存这个文件映射,实现共享内存,然后再使用信号量来同步。这个搭配可算是完美!

name

共享区名,需要绝对路径

和open文件一样,O_RDONLYO_RDWRO_CREATO_TRUNC

mode

权限位置,和文件相同。只能在O_CREAT下使用

成功返回一个文件描述字,失败负

既然它是一种文件,那么我们对文件操作的函数,fstatlstatreadwirteftruncate这些函数都可以尝试着对他使用一把。不成功便成人嘛!

5、删除共享内存区

intshm_unlink(constchar*name);

◆进程控制

linux进程控制包括创建进程,执行进程,退出进程以及改变进程优先级等。

在linux系统中,用于对进程进行控制的系统调用有:

a.fork:用于创建一个新进程。

b.exit:用于终止进程

c.exec:用于执行一个应用程序

d.wait:将父进程挂起,等待子进程终止

e.getpid:获取当前进程的进程ID

f.nice:该变进程的优先级

◆fork和vfork的区别

(1)fork():使用fork()创建一个子进程时,子进程只是完全复制父进程的资源。这样得到的子进程独立于父进程具有良好的并发性,父子进程执行顺序不定。

(2)vfork():使用vfork创建一个子进程时,操作系统并不将父进程的地址空间完全复制到子进程。而是子进程共享父进程的地址空间,即子进程完全运行在父进程的地址空间上,子进程对该地址空间中任何数据的修改同样为父进程所见。同时保证子进程先运行,在它调用exec或exit后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

◆exit()和_exit()的区别:

(1)exit函数有参数,正常终止进程,exit执行完后把控制权交给系统,exit是在_exit函数之上的一个封装,其会调用_exit,并在调用之前先刷新流。

(2)_exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin,stdout,stderr...)。

◆僵尸进程的避免

(1)父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起。

(2)如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。

(3)如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号。

(4)还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。

◆死锁的概念

◆死锁的四个必要条件:

互斥、占有、非抢占、循环等待

1、互斥使用(资源独占)

一个资源每次只能给一个进程使用

2、不可强占(不可剥夺)

资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放

3、请求和保持(部分分配,占有申请)

一个进程在申请新的资源的同时保持对原有资源的占有(只有这样才是动态申请,动态分配)

4、循环等待

存在一个进程等待队列

{P1,P2,…,Pn},

其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路

◆自旋锁

概述:自旋锁它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

自旋锁一般原理:

跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。由此我们可以看出,自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:死锁和过多占用cpu资源。

自旋锁适用情况:

◆线程的引入:

人们为了解决这个缺点,想到让进程在并行时不拥有资源---从而引入了线程的概念:即线程本身不拥有资源或者是很少的资源,进程是拥有资源的基本单位,线程是调度的基本单位.在操作系统中引入线程则是为了减少程序并发执行时所付出的时空开销,使操作系统具有更好的并发性。

◆线程标识

每个进程内部的不同线程都有自己的唯一标识,线程标识(ID)只在它所属的进程环境中有效,线程标识是pthread_t数据类型。

◆线程创建

新创建线程从start_rtn函数的地址开始运行,不能保证新线程和调用线程的执行顺序。

#include

intpthread_create(pthread_t*restricttidp,constpthread_attr_t*restrictattr,void*(*start_rtn)(void),void*restrictarg);

返回:成功返回0,否则返回错误编号

◆终止方式

pthread_exit和pthread_join的使用:

voidpthread_exit(void*retval)

intpthread_join(pthread_t*th,void**thread_return)

返回值:成功返回0,否则返回错误编号

pthread_exit:retval是pthread_exit调用者线程的返回值,可由其他函数和pthread_join来检测获取。线程退出时使用函数pthread_exit,是线程的主动行为。由于一个进程中的多个线程共享数据段,因此通常在线程退出后,退出线程所占用的资源并不会随线程结束而释放。所有需要pthread_join函数来等待线程结束,类似于wait系统调用。

pthread_join:等待线程的结束。

th:等待线程的标识符

thread_return:用户定义指针,用来存储被等待线程的返回值。

◆pthread_exit和pthread_cancel的区别

(1)线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。

(2)pthread_cancel是一种让线程可以结束其他线程的机制,一个线程可以对另一个线程发送一个结束的请求。当一个线程最终尊重了取消的请求,他的行为就像执行了pthread_exit函数。

◆线程的创建

void*print_xs()

fputc('x',stderr);

printf("hello1\n");

returnNULL;

pthread_tpthread_id;

pthread_create(&pthread_id,NULL,&print_xs,NULL);

//pthread_exit(NULL);

printf("hello2\n");

fputc('o',stderr);

1、这里的hello2会执行一次,在程序的开始打印,但hello1不会执行。

2、执行结果是:hello2ooooo……xxxxxxx…….oooo……xxxxxx

这就说明了:linux异步调度这两个程序,程序不能依赖执行的顺序

pthread_create()调用后会立刻返回,原线程会继续执行之后的指令,同时,新线程开始执行线程函数。

3、pthread_exit()(线程的退出)只会结束主线程,而不会结束创建的线程。

4、intpthread_create(pthread_t*thread_id,constpthread_attr_t*attr,void*(*start_routine)(void*),void*arg)

(1)pthread_t*thread_id:线程的标识符,一个指向pthread_t类型的指针,新线程的线程ID就存放在这里。

(2)constpthread_attr_t*attr:控制着新线程与程序其它部分交互的具体细节,若是NULL,新线程将被赋予一组默认属性。

(3)void*(*start_routine)(void*):线程执行的代码:线程函数,

这个函数所调用的函数类型是:void*(*function)(void*)传参过后再转化为我们需要的指针类型。

(4)void*arg:传递给线程函数的参数(这个模式不能改变)

◆joining线程

structchar_print

charc;

void*char_print(void*p)

structchar_print*pt=(structchar_print*)p;

for(i=0;inum;i++)

fputc(pt->c,stderr);

pthread_tthread1_id;

pthread_tthread2_id;

structchar_printt1;

structchar_printt2;

//创建一个输出1000个”#”的的线程

t1.num=1000;

t1.c='#';

pthread_create(&thread1_id,NULL,&char_print,&t1);

//创建一个输出1000个”*”的的线程

t2.num=1000;

t2.c='*';

pthread_create(&thread2_id,NULL,&char_print,&t2);

pthread_join(thread1_id,NULL);//等待线程1结束

pthread_join(thread2_id,NULL);//等待线程2结束

1、intpthread_join(pthread_tthread_id,void**value_ptr);

参数:线程ID和一个指向void*类型变量的指针,用于存放线性函数的返回值,如果是NULL,

2、参数的传递方式:这里是一个结构体t1和t2,将它们的地址&t1,&t2,传给线性函数void*char_print(void*p)中的p

3、线程函数:void*char_print(void*p):线程执行的代码

这个函数所调用的函数类型是:void*(*function)(void*)传参过后再转化为我们需要的指针类型,如果写成这样void*(*function)(structchar_print*),直接传参,将会出现警告。

3、正常情况下:执行态阻塞态结束态

(1)

Thread2

Thread1

结果:#######.......*******……#######(#和*总数都是1000个)

或者:##############......*****************……

(2)如果把线程1屏蔽掉pthread_join(thread1_id,NULL);会出现什么状况

结果:######......*******(*总数是1000个,但是#小于或等于1000个)

(3)如果把线程2屏蔽掉pthread_join(thread2_id,NULL);会出现什么状况

结果:#####......******……#####.....(#总数是1000个,但是*也是1000个)

或者:#############........(#总数是1000个但是*是0个)

4、教训:一旦你将对某个数据变量的引用传递给某个线程,务必确保这个变量不会被释放(甚至在其它线程中也不行!),直到你确定这个线程不会再使用它。这对于局部变量(当生命期结束的时候自动释放)和堆上分配的对象(free释放)也适用。

◆线程的取消

intpthread_cancel(pthread_tthread)

◆线程中的通信方式

互斥锁信号量条件变量

概述:线程间的通信目的主要是用于线程同步。所以线程没有像进程通信中的用于数据交换的通信机制。

为什么有了进程还要引入线程呢?

1)与进程相比,它是一个非常“节俭”的多任务操作方式,在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护代码段、堆栈段和数据段,这是一种“昂贵”的操作方式。

注意:需要头文件pthread.h连接时需要连接libpthread.a

◆互斥锁

#include

#defineN20

structQueue

intfront;

intrear;

intpQ[N];

structQueueQ1;

pthread_mutex_tjob_queue_mutex=PTHREAD_MUTEX_INITIALIZER;

int*produce();

int*consume();

pthread_tpthread_id1,pthread_id2;

Q1.rear=Q1.front=0;

pthread_create(&pthread_id1,NULL,(void*)produce,NULL);

pthread_create(&pthread_id2,NULL,(void*)consume,NULL);

pthread_join(pthread_id1,NULL);

pthread_join(pthread_id2,NULL);

int*consume()

intnum_get;

pthread_mutex_lock(&job_queue_mutex);

if(Q1.rear==Q1.front)

return;

num_get=Q1.pQ[Q1.front];

Q1.front=(Q1.front+1)%N;

printf("theconsumernumis:%d",num_get);

pthread_mutex_unlock(&job_queue_mutex);

int*produce()

intnum_creat;

num_creat=rand()%100;

if((Q1.rear+1)%N==Q1.front)

Q1.pQ[Q1.rear]=num_creat;

Q1.rear=(Q1.rear+1)%N;

printf("theproducenumis:%d\n",num_creat);

(1)互斥锁的初始化

方式一:

使用PTHREAD_MUTEX_INITIALIZER宏可以将以静态方式定义的互斥锁初始化为其缺省属性。

例程:pthread_mutex_tjob_queue_mutex=PTHREAD_MUTEX_INITIALIZER;

方式二:pthread_mutex_init(pthread_mutex_t*mp,constpthread_mutexattr_t*mattr)

可以使用缺省值初始化由mp所指向的互斥锁,还可以指定已经使用pthread_mutexattr_init()设置的互斥锁属性。mattr的缺省值为NULL。

如果互斥锁已初始化,则它会处于未锁定状态,互斥锁可以位于进程之间共享的内存中或者某个进程的专用内存中???。

初始化互斥锁之前,必须将其所在的内存清零,将mattr设置为NULL的效果与传递缺省互斥锁属性对象的地址相同,但是没有内存开销。

(2)使互斥保持一致

intpthread_mutex_consistent_np(pthread_mutex_t*mutex);

仅当定义了_POSIX_THREAD_PRIO_INHERIT符号时,pthread_mutex_consistent_np()才适用,并且仅适用于使用协议属性值PTHREAD_PRIO_INHERIT初始化的互斥锁。

调用pthread_mutex_lock()会获取不一致的互斥锁。EOWNWERDEAD返回值表示出现不一致的互斥锁。

持有以前通过调用pthread_mutex_lock()获取的互斥锁时可调用pthread_mutex_consistent_np()。

如果互斥锁的属主失败,则该互斥锁保护的临界段可能会处于不一致状态;在这种情况下,仅当互斥锁保护的临界段可保持一致时,才能使该互斥锁保持一致。

(3)锁定互斥锁(注意细节)

intpthread_mutex_lock(pthread_mutex_t*mutex);

当pthread_mutex_lock()返回时,该互斥锁已被锁定。调用线程是该互斥锁的属主。如果该互斥锁已被另一个线程锁定和拥有,则调用线程将阻塞,直到该互斥锁变为可用为止。

(4)解除锁定互斥锁

intpthread_mutex_unlock(pthread_mutex_t*mutex);

可释放mutex引用的互斥锁对象。互斥锁的释放方式取决于互斥锁的类型属性。如果调用pthread_mutex_unlock()时有多个线程被mutex对象阻塞,则互斥锁变为可用时调度策略可确定获取该互斥锁的线程。对于PTHREAD_MUTEX_RECURSIVE类型的互斥锁,当计数达到零并且调用线程不再对该互斥锁进行任何锁定时,该互斥锁将变为可用。

(5)销毁互斥锁

intpthread_mutex_destroy(pthread_mutex_t*mp);

信号量的数据类型为结构sem_t,它本质上是一个长整型的数。

例程1:

sem_tsem1,sem2;

void*pthread1(void*arg)

sem_wait(&sem1);

setbuf(stdout,NULL);

while(10>i++)

printf("hello\n");

sem_post(&sem2);

void*pthread2(void*arg)

sem_wait(&sem2);

printf("world\n");

pthread_tt1,t2;

sem_init(&sem1,0,1);

sem_init(&sem2,0,0);

pthread_create(&t1,NULL,pthread1,NULL);

pthread_create(&t2,NULL,pthread2,NULL);

pthread_join(t1,NULL);

pthread_join(t2,NULL);

sem_destroy(&sem1);

sem_destroy(&sem2);

(1)intsem_init(sem_t*sem,intpshared,unsignedintvalue);

例:sem_init(&sem1,0,1);

功能:用来初始化一个信号量

sem:为指向信号量结构的一个指针

pshared:共享选项,不为0时此信号量在进程间共享,为0时只能为当前进程的所有线程间共享

value:给出了信号量的初始值。

补充:这个函数的作用是对由sem指定的信号量进行初始化,设置好它的共享选项,并指定一个整数类型的初始值。pshared参数控制着信号量的类型。如果pshared的值是0,就表示它是当前进程的局部信号量;否则,其它进程就能够共享这个信号量。我们现在只对不让进程共享的信号量感兴趣。pshared传递一个非零将会使函数调用失败。

(2)intsem_wait(sem_t*sem);

功能:函数用于接受信号,当sem>0时就能接受到信号,然后将sem--;被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。

(3)sem_post(sem_t*sem)

功能:函数可以增加信号量,

补充:这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。

sem_post函数的作用是给信号量的值加上一个“1”,它是一个“原子操作"即同时对同一个信号量做加“1”操作的两个线程是不会冲突的;而同时对同一个文件进行读和写操作的两个程序就有可能会引起冲突。信号量的值永远会正确地加一个“2”--因为有两个线程试图改变它。

sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将会继续执行,信号量的值将减到1。如果对一个值为0的信号量调用sem_wait(),这个函数就会等待直到有其它线程增加了这个值使它不再是0为止。

如果有两个线程都在sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。

信号量这种“只用一个函数就能原子化地测试和设置”的能力下正是它的价值所在。还有另外一个信号量函数sem_trywait,它是sem_wait的非阻塞搭档。

(4)sem_destroy(sem_t*sem)

功能:解除信号量

补充:这个函数也使用一个信号量指针做参数,归还自己占据的一切资源。在清理信号量的时候如果还有线程在等待它,用户就会收到一个错误。与其它的函数一样,这些函数在成功时都返回“0”。

◆条件变量

使用条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的,条件变量始终与互斥锁一起使用,条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。

pthread_mutex_tjob_mutex=PTHREAD_MUTEX_INITIALIZER;

pthread_cond_tcond=PTHREAD_COND_INITIALIZER;

void*pthread1(void*argc)

intl=50,j;

while(l--)

j=10;

pthread_mutex_lock(&job_mutex);

printf("hello5");

while(i)

pthread_cond_wait(&cond,&job_mutex);

printf("hello6");

pthread_mutex_unlock(&job_mutex);

printf("hello7");

while(j--)

fputc('*',stderr);

i=1;

printf("hello8");

pthread_cond_signal(&cond);

pthread_tpthread_1;

pthread_create(&pthread_1,NULL,pthread1,NULL);

intl=50;

intj;

j=100;

printf("hello1");

while(!i)

printf("hello2");

printf("hello3");

fputc('#',stderr);

printf("hello4");

pthread_join(pthread_1,NULL);

(1)条件变量的结构为pthread_cond_t(相当于windows中的事件的作用)

(2)条件变量的初始化

intpthread_cond_init(pthread_cond_t*restrictcond,

constpthread_condattr_t*restrictattr);其中restrictcond是一个指向结构pthread_cond_t的指针,restrictattr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用,注意初始化条件变量只有未被使用时才能重新初始化或被释放。

(3)条件变量的释放

pthread_cond_destroy(pthread_cond_t*cond)释放一个条件变量

(4)条件变量的等待

函数pthread_cond_wait()使线程阻塞在一个条件变量上。它的函数原型为:

externintpthread_cond_wait_P((pthread_cond_t*__cond,pthread_mutex_t*__mutex));

例如:while(!i)

线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数pthread_cond_broadcast唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。

另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:

intpthread_cond_timedwait(pthread_cond_t*restrictcond,pthread_mutex_t*restrictmutex,

conststructtimespec*restrictabstime);

(5)条件变量的解除改变

函数pthread_cond_signal()的原型为:

intpthread_cond_signal(pthread_cond_t*cond);它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait函数之间被发出,从而造成无限制的等待。

下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。

pthread_mutex_tcount_lock;

pthread_cond_tcount_nonzero;

unsignedcount;

decrement_count()

pthread_mutex_lock(&count_lock);

while(count==0)

pthread_cond_wait(&count_nonzero,&count_lock);

count=count-1;

pthread_mutex_unlock(&count_lock);

increment_count(){

if(count==0)

pthread_cond_signal(&count_nonzero);

count=count+1;

count值为0时,decrement函数在pthread_cond_wait处被阻塞,并打开互斥锁count_lock。此时,当调用到函数increment_count时,pthread_cond_signal()函数改变条件变量,告知decrement_count()停止阻塞。

◆Linux和wondows进程线程间通信机制

Linux进程间通信

linux下进程间通信的几种主要手段简介:

a)管道(Pipe)及有名管道(namedpipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

b)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

c)Message(消息队列):消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

d)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

e)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

f)套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和SystemV的变种都支持套接字。

Linux线程间通信:互斥体,信号量,条件变量

Windows线程间通信:临界区(CriticalSection)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)

Windows进程间通信:管道、内存共享、消息队列、信号量、socket

Windows进程和线程共同之处:信号量和消息(事件)

临界区(Criticalsection)与互斥体(Mutex)的区别

1、临界区只能用于对象在同一进程里线程间的互斥访问;互斥体可以用于对象进程间或线程间的互斥访问。

2、临界区是非内核对象,只在用户态进行锁操作,速度快;互斥体是内核对象,在核心态进行锁操作,速度慢。

3、临界区和互斥体在Windows平台都下可用;Linux下只有互斥体可用

Windows线程间通信的区别:

1.互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。互斥体不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享.互斥量比临界区复杂

2.互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。

◆建立动态库

(1)建立动态库

hello.c

intprint(charc,intn)

while(n--)

printf("%c",c);

voidshow()

printf("Thisisatest!\n");

voidshow2()

printf("hahahhahhahahha\n");

建立方法:gcc-fpic-shared-olibhello.sohello.c

(2)建立测试文件

A、main.c直接加载共享库

print("Q",10);

测试方法:gcc-omainmain.c-L-lhello.so

B、main2.c搜索加载共享库

#include

int(*show)(void);

void*dlptr=NULL;

dlptr=dlopen("/usr/lib/libhello.so",RTLD_NOW);

show=dlsym(dlptr,"show2");

show();

测试方法:gccmain2.c-omain2-ldl

(2)执行文件

◆简单的学习makefile

相信在unix下编程的没有不知道makefile的,刚开始学习unix平台下的东西,了解了下makefile的制作,觉得有点东西可以记录下。

下面是一个极其简单的例子:

现在我要编译一个Helloworld,需要如下三个文件:

1.print.h

voidprinthello();

2.print.c

#include"print.h"

voidprinthello()

printf("Hello,world\n");

3.main.c

printhello();

好了,很简单的程序了。如果我们想要编译成功需要哪些步骤呢?

我认为在这里需要理解的就两步:

#为每一个*.c文件生成*.o文件。

#连接每一个*.o文件,生成可执行文件。

下面的makefile就是根据这样的原则来写的。

一:makefile雏形:

#makefile的撰写是基于规则的,当然这个规则也是很简单的,就是:

#target:prerequisites

command//任意的shell命令

实例如下:

makefile:

helloworld:main.oprint.o

#helloword就是我们要生成的目标

#main.oprint.o是生成此目标的先决条件

gcc-ohelloworldmain.oprint.o#shell命令,最前面的一定是一个tab键

main.o:main.cprint.h

gcc-cmain.c

print.o:print.cprint.h

gcc-cprint.c

clean:

rmhelloworldmain.oprint.o

OK,一个简单的makefile制作完毕,现成我们输入make,自动调用Gcc编译了,

输入makeclean就会删除hellowworldmian.oprint.o

二:小步改进:

在上面的例子中我们可以发现main.oprint.o被定义了多处,

我们是不是可以向C语言中定义一个宏一样定义它呢?当然可以:

gcc-ohelloworld$(objects)

mian.o:mian.cprint.h

rmhelloworld$(objects)

修改完毕,这样使用了变量的话在很多文件的工程中就能体现出方便性了。

三:再进一步:

再看一下,为没一个*.o文件都写一句gcc-cmain.c是不是显得多余了,能不能把它干掉?而且main.c和print.c都需要print.h,为每一个都写上是不是多余了,能不能再改进?能,当然能了:

objects=main.oprint.o

helloworld:$(objects)

$(objects):print.h#都依赖print.h

main.o:main.c#干掉了gcc-cmain.c让Gunmake自动推导了。

print.o:print.c

好了,一个简单的makefile就这样完毕了,简单吧。

◆线程和进程中的并发机制

#defineLOOP5

intnum=0;

intparm_0=0;

intparm_1=1;

pthread_mutex_tmylock=PTHREAD_MUTEX_INITIALIZER;

pthread_cond_tqready=PTHREAD_COND_INITIALIZER;

void*thread_func(void*arg)

inti,j;

for(i=0;i

pthread_mutex_lock(&mylock);

while(parm_0!=num)

pthread_cond_wait(&qready,&mylock);

printf("thread:\n");

for(j=0;j<10;j++)

printf("%d",j);

printf("\n");

num=(num+1)%2;

pthread_mutex_unlock(&mylock);

pthread_cond_signal(&qready);

return(void*)0;

inti,k;

pthread_ttid;

void*tret;

pthread_create(&tid,NULL,thread_func,NULL);

while(parm_1!=num)

printf("main:\n");

for(k=0;k<10;k++)

printf("%d",k+100);

pthread_join(tid,&tret);

-----------------------华丽的分割线------------------------

线程面试题:

1、线程的基本概念,线程的基本状态及状态之间的关系

3、进程的用户栈和内核栈

进程是程序的一次执行过程。用剧本和演出来类比,程序相当于剧本,而进程则相当于剧本的一次演出,舞台、灯光则相当于进程的运行环境。

进程的堆栈

每个进程都有自己的堆栈,内核在创建一个新的进程时,在创建进程控制块task_struct的同时,也为进程创建自己堆栈。一个进程有2个堆栈,用户堆栈和系统堆栈;用户堆栈的空间指向用户地址空间,内核堆栈的空间指向内核地址空间。当进程在用户态运行时,CPU堆栈指针寄存器指向的用户堆栈地址,使用用户堆栈,当进程运行在内核态时,CPU堆栈指针寄存器指向的是内核栈空间地址,使用的是内核栈;

当进程由于中断或系统调用从用户态转换到内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断,称为软中断。进程因为中断(软中断或硬件产生中断),使得CPU切换到特权工作模式,此时进程陷入内核态,进程进入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置堆栈指针寄存器的地址为内核栈地址,这样就完成了用户栈向内核栈的切换。

当进程从内核态切换到用户态时,最后把保存在内核栈中的用户栈地址恢复到CPU栈指针寄存器即可,这样就完成了内核栈向用户栈的切换。

这里要理解一下内核堆栈。前面我们讲到,进程从用户态进入内核态时,需要在内核栈中保存用户栈的地址。那么进入内核态时,从哪里获得内核栈的栈指针呢?

理解了从用户态刚切换到内核态以后,进程的内核栈总是空的,那刚才这个问题就很好理解了,因为内核栈是空的,那当进程从用户态切换到内核态后,把内核栈的栈顶地址设置给CPU的栈指针寄存器就可以了。

X86Linux内核栈定义如下(可能现在的版本有所改变,但不妨碍我们对内核栈的理解):

在/include/linux/sched.h中定义了如下一个联合结构:

uniontask_union{

structtask_structtask;

unsignedlongstack[2048];

从这个结构可以看出,内核栈占8kb的内存区。实际上,进程的task_struct结构所占的内存是由内核动态分配的,更确切地说,内核根本不给task_struct分配内存,而仅仅给内核栈分配8K的内存,并把其中的一部分给task_struct使用。

这样内核栈的起始地址就是uniontask_union变量的地址+8K字节的长度。例如:我们动态分配一个uniontask_union类型的变量如下:

unsignedchar*gtaskkernelstack

gtaskkernelstack=kmalloc(sizeof(uniontask_union));

那么该进程每次进入内核态时,内核栈的起始地址均为:(unsignedchar*)gtaskkernelstack+8096

进程上下文

进程切换现场称为进程上下文(context),包含了一个进程所具有的全部信息,一般包括:进程控制块(ProcessControlBlock,PCB)、有关程序段和相应的数据集。

进程控制块PCB(任务控制块)

进程的切换

Linux的进程切换是通过调用函数进程切换函数schedule来实现的。进程切换主要分为2个步骤:

1.调用switch_mm()函数进行进程页表的切换;

2.调用switch_to()函数进行CPU寄存器切换;

__switch_to定义在/arch/arm/kernel目录下的entry-armv.S文件中,源码如下:

-----------------------------------------------------------------------------

ENTRY(__switch_to)

UNWIND(.fnstart)

UNWIND(.cantunwind)

addip,r1,#TI_CPU_SAVE

ldrr3,[r2,#TI_TP_VALUE]

stmiaip!,{r4-sl,fp,sp,lr}@Storemostregsonstack

#ifdefCONFIG_MMU

ldrr6,[r2,#TI_CPU_DOMAIN]

#endif

#if__LINUX_ARM_ARCH__>=6

#ifdefCONFIG_CPU_32v6K

clrex

#else

strexr5,r4,[ip]@Clearexclusivemonitor

#ifdefined(CONFIG_HAS_TLS_REG)

mcrp15,0,r3,c13,c0,3@setTLSregister

#elif!defined(CONFIG_TLS_REG_EMUL)

movr4,#0xffff0fff

strr3,[r4,#-15]@TLSvalat0xffff0ff0

mcrp15,0,r6,c3,c0,0@Setdomainregister

movr5,r0

addr4,r2,#TI_CPU_SAVE

ldrr0,=thread_notify_head

movr1,#THREAD_NOTIFY_SWITCH

blatomic_notifier_call_chain

movr0,r5

ldmiar4,{r4-sl,fp,sp,pc}@Loadallregssavedpreviously

UNWIND(.fnend)

ENDPROC(__switch_to)

----------------------------------------------------------

Switch_to的处理流程如下:

1.保存本进程的CPU寄存器(PC、R0~R13)到本进程的栈中;

2.保存SP(本进程的栈基地址)到task->thread.save中;

3.从新进程的task->thread.save恢复SP为新进程的栈基地址;

5.新进程开始运行,完成任务切换。

这里读者可能会问,在进行任务切换的时候,到底是在运行进程1还是运行进程2呢?进程切换的时候,已经进行页表切换,那页表切换之后,切换进程使用的是进程1还是进程2的页表呢?

要回答这个问题,首先我们要明白由谁来完成进程切换?

◆内核态和用户态

intelx86架构的CPU分Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。

例如:版主可以删除你的贴子,你却不能删除版主的贴子,当然你也没法让自己变成版主,除非版主把你升为版主(或是系统有bug).....例如:这个论坛是由版主创立的,当时创立时就他一个用户,所以它就自然取得了最高权限了,即版主运行在Ring0级(内核态),然后他再让我们注册自己的账号,但是设置的权限是普通用户,所以我们一注册就只能是普通用户,即我们只能运行在Ring3级(用户级),我们没有更多的权限,所以我们只能任由版主"蹂躏"了,看你不爽就删除你的贴子,踢你下线,加你进黑名单,永久封你的IP之类的,你除了发发牢骚之外也是无可奈何了,这就是操作系统在内核态可以管理用户程序,杀死用户程序的原因了。Linux环境下的内核态与用户态

◆linux允许每个线程有多大的线性地址空间?

■库函数

我认为fopen(库函数)和open(系统调用)最主要的区别是fopen在用户态下就有了缓存,在进行read和write的时候减少了用户态和内核态的切换,而open则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列快;如果随机访问文件open要比fopen快。

fopen是有缓冲机制的,它使用了FILE这个结构才保存缓冲数据。open没有缓存机制,每次读操作都直接从文件系统中获取数据。看一下FILE这个结构的定义就知道区别了,FILE包含了一个open返回的handle

■系统调用数

在2.6.32内核中,共有系统调用360个,可在arch/arm/include/asm/unistd.h中找到它们。

■系统调用的工作原理

一般情况下,用户进程是不能访问内核的。它既不能访问内核所在的内存空间,也不能调用内核中的函数。系统调用是一个例外。其原理是进程先用适当的值填充寄存器,然后调用一个特殊的指令,这个指令会让用户程序跳转到一个事先定义好的内核中的一个位置。

1)在IntelCPU中,这个指令由中断0x80实现。

2)在ARM中,这个指令是SWI。

例如:在进程运行时在用户空间调用open(每一个函数对应一个系统调用号,如open为5),进程可以跳转到的内核位置是ENTRY(vector_swi),这个过程检查系统调用号,这个号码5告诉内核进程请求是open这种服务,然后,它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址,接着,就调用函数,等返回后,做一些系统检查,最后返回到进程。

THE END
1.由于设备驱动程序的前一个实例仍在内存中,Windows无法加载这个...里面显示"由于设备驱动程序的前一个实例仍在内存中,Windows 无法加载这个硬件的设备驱动程序。"如果硬盘...http://bbs.wuyou.net/forum.php?mod=viewthread&tid=194694
1.内存相关问题分析流程10% 26655/kworker/3:40H-kverityd: 0% user + 10% kernel 7.6% 744/loop32: 0% user + 7.6% kernel 93% TOTAL: 10% user + 38% kernel + 40% iowait + 1.5% irq + 1% softirq swt 前cpu一直high loading,发生大量anr,其中都是kswapd top,io wait高也是因为内存回收,导致read行为不断重复堆...https://blog.csdn.net/tianshi4851/article/details/143571608
2.解析指令引用内存错误,原因与解决方法详解2、电脑总是出现0x00000000指令引用内存错误,通常是由于软件冲突、内存问题、驱动程序错误或系统文件损坏等原因造成的。详细 当电脑出现0x00000000指令引用内存错误时,这通常意味着某个程序试图访问一个无效或不存在的内存地址。这种情况可能由多种原因引起。 http://www.qyunz.com/f27B133b399e.html
3.記憶體用量最佳化AndroidDevelopers請避免使用System.gc()和直接釋放記憶體呼叫,因為這會干擾系統的記憶體處理程序:舉例來說,在使用 zRAM 的裝置中,強制呼叫gc()可能會因記憶體的壓縮和解壓縮而暫時增加記憶體用量。 ,例如 Compose 中的目錄瀏覽器或現已淘汰的 Leanback UI 工具包中的RecyclerView,以便重複使用檢視畫面,而非重新建立清單元素。 https://developer.android.google.cn/training/tv/playback/memory?tab=t.0
4.由于启动计算机出现了页面文件配置问题,高度郑重详细:版辕97.30...页面文件是操作系统用来管理内存的一种方式,当系统的物理内存不足时,操作系统会将一部分不常用的内存数据存储在页面文件中,当系统需要这些数据时,再从页面文件...配置问题,这可能是由于硬件与软件的兼容性问题、系统文件的损坏或病毒感染等原因导致的,具体表现为计算机启动时出现错误提示,无法正常加载操作系统或应用程序。https://ci.bolei.cc/post/2989.html
5.解决Windows7“内存位置访问无效”错误的全面指南与预防措施...二、当遇到“内存位置访问无效”怎么办? 面对这一问题,用户可以根据以下步骤进行排查和解决: 1. 重启计算机 有时候,简单的重启就能解决许多问题。如果是偶然发生的错误,可以尝试重启计算机,看看问题是否依然存在。 2. 更新驱动程序 过时或不兼容的驱动程序可能是导致错误的原因之一。打开“设备管理器”,检查是否有设备...http://www.jieyangjs.cn/gonglue/3315.shtml
6.如何解决开机时出现的EAccessViolation错误?EAccessViolation错误是一种在计算机程序中常见的运行时错误,通常发生在程序试图访问非法或没有权限的内存地址时。这种错误不仅会导致程序崩溃或异常终止,还可能影响到系统的稳定性和安全性。尤其在开机时遇到EAccessViolation错误,往往会让人感到困扰。那么,如何有效地消除开机时的EAccessViolation错误呢?以下是一些实用的方法和...https://www.mwshe.cn/shgw/667077.html
7.技嘉bioscheckerromob64ca12ecf3b4的技术博客在上述代码中,我们使用了psutil库来检查计算机的内存和硬盘状态。通过运行此代码,用户可以获得关于硬件状态的基本信息。 流程图 为了解决技嘉BIOS检查错误,下面是一个简化的流程图,展示了如何逐步解决此错误的方法。我们将使用Mermaid语法来表示流程。 是否是否是否检查硬件连接是否连接正常?恢复默认BIOS设置重新连接硬件是否...https://blog.51cto.com/u_16213417/12700710
8.停止错误Bug检查或蓝屏错误高级疑难解答有关详细信息,请参阅如何在 Windows 中执行干净启动。 可以按照如何在 Windows 中暂时停用内核模式筛选器驱动程序中的步骤禁用驱动程序。 可能还需要考虑回滚更改或还原到最后已知工作状态的选项。 有关详细信息,请参阅将设备驱动程序回滚到以前的版本。 内存转储收集 ...https://docs.microsoft.com/zh-cn/windows/client-management/troubleshoot-stop-errors
9....由于设备驱动程序的前一个实例仍在内存中,windows无法加载...笔记本电脑识别不了刻录机,由于设备驱动程序的前一个实例仍在内存中,windows 无法加载这个硬件的设备驱动程序。 (代码 38) 笔记本电脑插入刻录机,一直识别不了; 右边点击属性 这里展示 由于设备驱动程序的前一个实例仍在内存中,windows 无法加载这个硬件的设备驱动程序。 (代码 38) ...https://cloud.tencent.com/developer/article/2352235
10.遇到803报错,我该如何解决?一、 803错误通常指的是在软件或硬件操作中遇到的特定错误代码,它可能由多种原因引起,为了更准确地理解和解决这个问题,我们需要从多个角度进行深入分析。 (图片来源网络,侵权删除) 二、常见原因分析 1. 软件冲突 2. 硬件故障 3. 操作系统问题 4. 驱动程序问题 ...https://blog.huochengrm.cn/gz/13856.html
11.设备驱动程序的前一个实例仍在内存中,Windows无法加载这个硬件的...6条回答:【推荐答案】把以前的删了重装https://wap.zol.com.cn/ask/x_10494436.html
12.>第2章Solaris运行时问题从可移除介质文件系统的顶层目录运行volstart程序。 按照CD 附带的说明,从 CDE 的外部访问。 Solaris PDASync 不能从桌面删除最后一项 (4260435) 当从桌面删除最后一项后,在同步手持设备时,该项会从手持设备恢复到桌面。例如,“日历”中的最后一个约会或“地址管理器”中的最后一个地址。 https://docs.oracle.com/cd/E19253-01/820-1877/6ndh3vjte/index.html
13.构建VMware混合云平台由于有两种类型的内存可供系统独立使用,因此该工作模式要求操作系统和应用提供额外支持。低时延操作应导向 DRAM,而需要持久性的数据或非常大的结构则应导向持久内存。在该模式下,即使系统断电,存储在持久内存模组上的数据仍具备持久性。应用可通过标准的操作系统存储调用来访问持久内存,就像访问一般存储设备上的一个普通...https://www.intel.cn/content/www/cn/zh/cloud-computing/building-a-vmware-hybrid-cloud-platform.html
14.AWR报告详解柏林之花library hit表示Oracle从Library Cache中检索到一个解析过的SQL或PL/SQL语句的比率,当应用程序调用SQL或存储过程时,Oracle检查Library Cache确定是否存在解析过的版本,如果存在,Oracle立即执行语句;如果不存在,Oracle解析此语句,并在Library Cache中为它分配共享SQL区。低的library hit ratio会导致过多的解析,增加CPU消耗...http://blog.chinaunix.net/uid-7847832-id-3486670.html
15.电脑主板出错怎么解决总纲大全,不可错过!主板又称母板,顾名思义是电脑的母体,其上载有CPU、内存、各种板卡及与之连接的外部设备。因此,它即是电脑系统的重要组成部分,又是故障涉及面最多的配件。下面我把自己对这个错综复杂的故障体的认识奉献给大家,一起来看看吧! 主板与https://www.xuexila.com/it/zhuban/c66661.html
16.电脑一打开软件就死机的解决方法(3) USB设备的驱动程序安装不正确。 类似上面的显卡的介绍。 6.杀毒软件或其他防火墙安装设置不正确 这个我们遇到的比较多。在我自己的实际工作中,诺顿没有出现过死机现象,瑞星有时会导致系统运行速度极慢,甚至死 机(我遇到过一次,在安装完瑞星后,加载WPS2000竟然长达六七分钟才能完成启动),毒霸好像是问题最多的,...https://www.jy135.com/diannao/28094.html
17.第9章编写FreeBSD设备驱动程序FreeBSDDocumentationPortal这个简单的伪设备例子会记住你写给它的任何值,并且当你读取它的时候 会将这些值返回给你。下面显示了两个版本,一个适用于FreeBSD 4.X, 一个适用于FreeBSD 5.X。 例1. 适用于FreeBSD 4.X的回显伪设备驱动程序实例 /* * 简单‘echo’伪设备KLD https://www.freebsd.org/doc/zh_CN/books/arch-handbook/driverbasics.html
18.SUNSolaris问题小结-F好像是一个修复参数 -F ufs 是文件格式 -y 不要你敲y了,全部自动yes fsck是对news过程的一个恢复 这两个操作是对裸设备文件的。 7) Q: 在solaris9上怎样设置oracle10g随系统启动时自动启动? A: 设定自启动 1. 先在/etc/init.d目录下,作下一个shell文件, ...http://www.cnetcom.cn/news/Technology/926.html