嵌入式软件开发工程师 ——“面经宝典“ 面试神器 (持续更新..)
本文总共分,c语言,数据结构,linux系统,计算机网络,ARM体系架构**这五个模块,当然每次面试过程中主要考的还是**c语言和linux相关知识**,在学习linux方面未触及到内核和驱动,后续还会进行补充,也当是给自己的一个总结。所以本文阐述c语言知识和linux知识居多。
面经宝典
本文总共分, c语言,数据结构,linux系统,计算机网络,ARM体系架构这五个模块,当然每次面试过程中主要考的还是 c语言和linux相关知识,在学习linux方面未触及到内核和驱动,后续还会进行补充,也当是给自己的一个总结。所以本文阐述c语言知识和linux知识居多。 因为本人能力有限,仅对本科这个阶段的一些面试的公司岗位进行了总结,如有错误,还望指正。
第一部分:c语言基础考题
1.预处理
1. 预编译,编译过程最先做的工作是啥?
答:编译#字开头的指令,如
拷贝#include包含的头文件代码,#define宏定义的替换,条件编译ifndef
何时需要预编译 ?
答:
①总是经常使用但是不经常改动的大型代码。
②程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选项,将所有包含文件预编译为一个 “预编译头”。
2. c语言中 # 与 ##的区别以及作用
答:
#
:把宏参数变成一个字符串;
##
:把两个宏参数连接到一起(只能两个)
例:
#define hehe(x,y) x##y
int main()
{
char string[]="hello world!";
printf("%s\n",hehe(str,ing));
system("pause");
return 0;
}
3.如何避免头文件被重复包含?
答:解决方法:
应用#ifndef
#define
#endif
2. 关键字
1.static关键字的作用?
答:static最主要功能是隐藏,其次因为static变量存放在静态存储区,具备持久性和默认值为0
①隐藏作用,可以在不同的文件中定义同名变量和同名函数。
②对于变量来说,保持变量持久,静态数据区的变量会在程序刚刚运行时就完成初始化,也是唯一一次初始化;储存在静态数据区,静态存储区只有两种变量(全局变量和static静态变量)。
③默认初始化为0x00,和全局变量一样的属性,减少程序员的工作量。
2.const关键字的作用
答:
①对变量加以限定不能被修改,常量必须在定义的时候同时被初始化。
②const和指针一起使用,
const int *p1;
int const *p2;
int *const p3;
在三种情况中,第三种指针是只读的,p3本身的值不能被修改;
第一二种情况,指针所指向的数据是只读的,p1,p2的值可以修改,但指向的数据不能被修改。
③const和函数形参一起使用
使用const单独定义变量可以用#define命令替换,const通常放在函数形参中。
如果形参是一个指针,为了防止在函数内部修改指针指向的数据就可以用const来限制。
3.volatile关键字的作用?
编译器优化的介绍: 内存访问速度远远比不上cpu处理的速度,为了提高性能,从硬件上引入高速缓存cache,加速对内存的访问。
编译优化的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用CPU指令流水线。
答:因为访问寄存器要比访问内存单元要快的多,编辑器会作减少存取的优化。
当使用volatile声明函数变量的时候,系统总是重新从它所在的内存读取数据。遇到这个关键字声明的变量,编译器对访问该变量的代码不再进行优化,从而提供对特殊地址的稳定访问; 如果不使用valatile,编译器将对所声明的语句进行优化,以免出错。
4.extern关键字的作用?
答:
①函数内的局部变量,函数外定义的变量为全局变量,为静态存储方式,生存周期为整个程序,有效范围为定义变量的位置开始到本源文件结束。
如果在定义前想要引用该全局变量,则应该加上 extern作为 “外部变量声明”。
多个源文件的工程想要引用一个源文件的外部变量也只许引用变量的文件中加入extern关键字加以声明,但是可以在引用的模块内修改其变量的值,慎用。
②extern “C”: C++代码调用C语言代码。在C++的头文件中使用。
5.sizeof关键字的作用?
答:sizeof 在 编译阶段处理,作用为取得一个对象(数据类型或数据对象)的长度(即占用内存的大小,以1字节为单位)。
①指针可以看做变量的一种,32位操作系统sizeof 指针都为4,例子:
int *p;
sizeof(p) =4;
sizeof(*p) = sizeof(int )=4;
②对于静态数组,sizeof可以直接计算数组大小,例:
int a[10];
char b[ ]= “hello”;
Sizeof (a) = 4 * 10 =40;
Sizeof (b) = 6; (求字符串长度时要加上字符串结束符/0)
③数组作为函数形参时候,数组名当做指针 使用,例:
Void fun (char p[ ])
{
Sizeof (p) ; //结果为4
}
④ sizeof 与 strlen 的区别:
*sizeof 是操作符, strlen为函数;
*sizeof 可以使用类型作为参数,如int char;
strlen只能使用 char*做参数且以\0为结尾
*sizeof 为数组时候,不退化, 传递给strlen时数组会被退化橙指针;
3. 结构体
1.结构体的赋值方式:
①初始化:如:
struct st{
char a;
int b;
}x={'A', 1};
②定义变量后按字段赋值,如:
struct st{
char a;
int b;
};
struct st x;
x.a = 'A';
x.b = 1;
③结构体变量的赋值,如:
struct st {
char a;
int b;
};
struct st x,y;
x.a= 'A';
x.b=1;
y=x;
2.结构体位域
位域类型: char、 short、int 可带上(signed或者unsigned)
位域的定义:
struct st{
Unsigned char a:7; //字段a占用一个字节的 7bit(位)
Unsigned char b:2; //字段b占用一个字节的 2ibt(位)
Unsigned char c:7;
}s1;
位域的好处:
①并不需要完整的字节,节省存储空间,处理简单;
②方便利用位域把一个变量按位域分解;
但是不利于程序的移植!!
3.计算一个结构体大小,sizeof(struct s1)
①结构体偏移量的概念:
结构体中的偏移量指的是一个成员的实际地址和结构体首地址之间的距离。
②结构体大小的计算方法:
结构体会涉及到字节对齐,(目的是为了让计算机快速读取,用空间交换速度),即最后一个成员的大小+最后一个成员的偏移量+末尾的填充节数。
③结构体内偏移规则
第一步:每个成员的偏移量都必须是当前成员所占内存大小的整数倍,如果不是整数倍,编译器会在前填充字节。
第二步:当所有成员大小计算完毕后,编译器会对当前结构体大小是否是最宽的结构体成员的大小的整数倍,如果不是整数倍,编译器会填充字节。
例:
Struct s1{ 成员的大小 成员的偏移量
int a; 4 0
char b; 1 4
int c; 4 5+3(填充字节)
long d; 8 12+4(填充字节)
char e; 1 24
}
sizeof(s1)=24+1+7(填充字节)
4.结构体成员数组大小
①
struct st {
int a;
int b;
char c[0];
}st_t;
sizeof(st_t) = 8
②
struct book {
char name[5];
float price;
}book[2];
sizeof( book ) = 12;
sizeof (book[2]) = 24;
第一步:结构体成员占字节数族大的元素为 sizeof ( float ) = 4;则用4 来分配其他成员;
第二步:数组超过四个字节,换行继续,剩下是哪个字节不够float型数据使用,则换行。
如果还不清楚,看下面的详细案例:
面试葵花宝典之结构体大小的计算方式
int : 4个字节
float: 4个字节
char: 1个字节
double:8个字节
在实际生活中,保存的数据一般不会是同一一种类型,所以引入了结构体。而结构体的大小也不是成员类型大小的简单相加。需要考虑到系统在存储结构体变量时的地址对齐问题。
由于存储变量地址对齐的问题,结构体大小计算必须满足两条原则:
一. 结构体成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
二. 结构体大小必须是所有成员大小的整数倍(数组 和 结构体 除外)。
三. 对齐方式很浪费空间 可是按照计算机对内存的的访问规则这样的对齐却提高了效率
练习一:
struct s1{
char ch1; //char 1字节
char ch2; //char 1字节
int i; //int 4字节
};
遵循结构体运算法制第一条,偏移量必须是当前成员变量的大小整数倍,逻辑 偏移2 实际按照对其的规则,要偏移4
这个结构体的大小容易计算,满足两个原则即可,为8
练习二:
struct s2{
char ch1; //char 1字节 【】 +【】【】【】 【】【】【】【】
int i; //int 4字节 //4+3
char ch2; //char 1字节 //1 8+1=9? 因为第2个原则 所以变成12;
};
这个结构体大小是12,为社么呢?仔细看看两个原则,要满足偏移量是成员的整数倍,ch1偏移量是0 ,i的偏移量不可能是1,因为1不是i大小4的倍数,所以i的偏移量是4,ch2的偏移量就变成了8,所以为了满足结构体大小是成员大小整数倍,就是12;
练习三:成员包含数组的结构体
struct s3{
char ch; //char 1 【】 +【】【】【】 【】【】【】【】
int i; //4+3
char str[10]; // 10 10+8=18 因为第2个原则 所以变成20;
};
这个结构体的大小是20,先看前两个成员,大小是8,毋庸置疑,这个char类型的数组,只需要把它看做成十个char连在一起即可,加起来就是18,在满足结构体大小为成员整数倍,所以大小就是20;
练习四:成员包含结构体的结构体
struct s4{
char ch; //1
int i; //4+3
struct s{
char ch1; //1
int j; //4+3
};
float f; //4 8+8+4=20? 是8+4=12
//因为内嵌的结构体它只是声明,不占空间。所以是不算它的大小所以是8+4=12
};
这个内嵌的结构体的大小是8,那么是否结构体大小就要向8对齐?
这个结构体大小是12,为什么看这是20,确是12呢,因为内嵌的结构体它只是声明,不占空间。只是作为一个代码,所以是不算它的大小。
若代码改为
struct s4{
char ch; //1
int i; //4+3
struct s{
char ch1; //1
int j; //4+3
}stmp;
float f; //4 8+8+4=20
};
这个内嵌的结构体的大小是8,那么是否结构体大小就要向8对齐?
这个结构体的大小是20.很明显不是8的倍数。所以计算结构体大小时是把里面这个结构体就看做是一个char 和一个int,不是看做一个整体.
练习五:成员包含联合体的结构体
struct s5{ //4+3+1=8
char ch; //1
int i; //4+3
union{ //联合体按最大成员的字节数算 所以是4
char ch1; //1
int j; //4
};
};
联合体大小就是成员中最大类型的大小,8+4=12,所以这个结构体大小是12
练习六:指定对齐值
(1)对齐值小于最大类型成员值
//当成员的大小超过了pack要求指定大小 就按pack要求的对齐
#pragma pack(4) //指定向4对齐 最大是8 超过了指定大小
struct s6 {
char ch; //1
int i; //4+3
float f; //4
double d; //8 8+4+8=20 --24 由于是指定向4对齐 所以是20
};
(2)对齐值大于最大类型成员值
//当成员的大小没有超过pack要求指定大小 结构体的总大小就按成员最大的对齐
#pragma pack(10) //指定向4对齐 最大是8 没有超过了指定大小
struct s7 {
char ch; //1
int i; //4+3
float f; //4
double d; //8 8+4+8=20 --24 所以是24
};
我们指定的对齐值是10 最大为8, 是否就向10对齐? 不是 当对齐值大于最大类型成员值时 向自身对齐值对齐 大小为24
总体来说,向指定对齐值和自身对齐值中较小的那个值对齐
注:面试官常问的C语言基本概念
1.引用与指针的区别:
①引用必须初始化,指针不必初始化
②引用初始化后不能改变,但是指针可以改变所指的对象
③不存在空值的引用,但是存在空值的指针
2…h头文件中, ifndef /define /endif的作用
①防止头文件被重复调用
3.include<file.h>和includ”file.h”的区别?
①前者从编译器自带的库函数中寻找文件,从标准库路径开始搜索文件
②后者是从自定义的文件中寻找文件,寻找不到再到库函数中寻找文件
4.全局变量和局部变量的区别?
①全局变量->存储在静态数据区,占用静态的存储单元
②局部变量->存储在栈中,只有在函数被调用过程中才开始分配存储单元
5.堆栈溢出的原因有哪些?
①函数调用层次太深,函数递归调用时,系统要在栈中不断保存函数调用时的线程和产生的变量,递归调用太深,会造成栈溢出,这是递归无法返还。
②动态申请空间使用后没有释放。由于C语言中没有垃圾资源自动回收机制,因此需要程序员主动释放已经不再使用的动态地址空间。
③数组访问越界,C语言没有提供数组下标越界检查,如果在程序中出现数组下标访问超出数组范围,运行过程中可能会存在内存访问错误。
④指针非法访问,指针保存了一个非法地址,通过这样的指针访问所指向的地址时会产生内存访问错误。
6.队列和栈的区别
相同点: 栈和队列是限定插入和删除只能在表的“端点”进行的线性表
不同点: 栈 元素必须 “先进后出”, 队列 元素必须 “先进先出”
7.局部变量能否与全局变量重名?
能,
局部变量会屏蔽 全局变量,要使用全局变量,需使用”::”;
局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。
8.如何引用一个已经定义了的全局变量?
①用extern 关键字方式
②用引用头文件方式,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。
9.全局变量可不可以定义在可被多个.c文件包含的头文件中,为啥?
可以,在不同的C文件中各自用static声明的全局变量,变量名可能相同,但是各自C文件中的全局变量的作用域为该文件,所以互不干扰。
10.Do…while 和while …do的区别?
①do …while :循环一次再进行判断
②while …do : 先判断再进行循环
11.static 关键字在 全局变量、局部变量、函数的区别?
①全局变量+static :改变作用域,改变(限制)其使用范围。 只初始化一次,防止在其他文件单元中被引用。
全局变量的作用域是整个源程序,在各个源文件中都是有效的,而加了静态后的全局变量的作用域 仅限于一个源文件中。
②局部变量+static:改变看它的存储方式,也就是改变了它的生存期。 ③普通函数+static :作用域不同,仅在本文件。
只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义,对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。
12.程序的内存分配(常考)
前言:c语言程序.c文件经过编译连接后形成编译、链接后形成的二进制映像文件由堆,、栈、数据段(只读数据段,未初始化数据段BSS,已初始化数据段三部分)、代码段组成。
①栈区 (stack):由编译器进行管理,自动分配和释放,存放的是 函数调用过程中的各种参数,局部变量,返回值以及函数返回地址。
②堆区(heap) :用于程序 动态申请分配和释放空间,malloc和free,程序员申请的空间在使用结束后应该释放,则程序自动收回。
③全局(静态)存储区:分为 DATA(已经初始化),BSS(未初始化)段,DATA段存放的是全局变量和静态变量;
BSS(未初始化)存放未初始化的全局变量和静态变量。 程序运行结束后自动释放,其中BSS(全部未初始化区)会被系统自动清零。
④文字常量区 :存放常量字符串,程序结束后由系统释放。
⑤程序代码段:存放程序的二进制代码。
程序的内存分配
代码在内存种从高地址—>低地址分区为栈区,堆区,全局区,常量区,代码区
- 栈区:函数的参数,局部变量和它的返回值的是存在栈区的,栈区的内容都是由系统自动分配,由系统自动释放的。栈区的好处是,执行效率高,速度快。
- 堆区:它是由程序员去开辟和释放的,由堆申请的一块内存属于动态内存,好处是申请比较灵活,而且使用非常方便。一般用
malloc
或者new
去开辟一段空间。存在堆区,在使用完这个区域后必须要用free
或者delete
去释放这个区域。不然就会造成内存泄漏,造成我们堆区越用越少,直到最后整个程序崩溃。
还有就是频繁的申请不同大小的堆空间,也会造成我们的内存碎片越来越多。 - 全局区: 全局区的内存是我们程序被编译的时候就已经分配了,同时也是在我们程序运行前就已经分配好了,它在我们整个运行期间都是存在的,在全局区里,很多内容基本上是我们全局变量和static修饰的变量,(全局区又称为静态存储区,分为初始化后的变量存储区和未初始化的变量存储区。)
- 常量区: 数字常量和字符常量所有常量都存在常量区里
- 代码区: 写代码的时候,必不可免的遇到很多函数,所有这些函数的二进制代码都会存在我们的代码区。
int a = 0; //全局初始化区
char *p1; //全局未初始化区
int main() //代码区
{
int b; //栈区(局部变量)
char s[]="abc"; //s数组在栈区 'abc\0'在常量区
char *p2; //栈区(局部变量)
char *p3 = "123456"; //指针p3在栈区(局部变量) '123456\0'在常量区
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10); //分配的10字节区域在堆区 p1在全局区
p2 = (char *)malloc(20); //分配的20字节区域在堆区 p2在栈区
strcpy(p1,"123456"); //'123456\0'在常量区 系统可能会把它和p3的字符放一起
free(p1);
free(p2);
}
13.解释”堆”和”栈”的区别:
注:被问到这个问题的时候可以从这几个方面进行阐述
①申请方式②申请后的系统反应③申请内存的大小限制④申请效率⑤存储内容⑥分配方式
①申请方式:
Strack(栈): 由编译器自带分配释放,存放函数的参数值,局部变量等。
Heap(堆):程序员自己申请,并指名大小–>malloc函数。
②申请后的系统响应
Strack(栈):只要栈剩余空间>所申请空间,都会提供。
Heap(堆):操作系统有记录空闲内存的链表:收到申请->遍历链表->寻找->申请空间的堆结点
③申请内存的大小限制
Strack(栈):向低地址扩展的数据结果,连续内存区域,栈 获得的空间较小。
Heap(堆):向高地址扩展的,不连续内存区域;链表遍历方向为(低地址->高地址)。 栈获得空间灵活,空间也大。
④申请效率
Strack(栈):系统自由分配,速度快。 Heap(堆):速度慢,容易产生内存碎片。
⑤存储内容 Strack(栈):
第一进栈 :主函数中的下一条指令的地址
–>函数的各个参数,参数由右往左进栈。–>函数的局部变量(静态变量不入栈)。调用结束后,顺序相反,局部变量先出栈。 Heap(堆): 程序员自己安排
⑥分配方式 Strack(栈):
栈有两种分配方式,静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配,动态分配由alloca函数进行分配,但栈的动态分配和堆是不同的,栈的动态内存由编译器进行释放,无需手工实现。Heap(堆):堆都是动态分配的,没有静态分配的堆。
14.结构体和联合体的区别:
①结构体和联合体:
都是由不同的数据类型成员组成,但是在同一时刻,联合体只存放了一个被选中的成员(所有成员公用一块地址);而结构体成员都存在(不同成员存放地址不同)。
例子:
union abc{
int i;
char m;
};
*在联合体abc中,整形量i和字符m共用一块内存位置。
*当一个联合体被说明时,编译程序自动产生一个变量,其长度为联合体中最大的变量长度
②联合体不同成员赋值,会对其他成员重写,原来成员的值会不存在。
结构体的不同成员赋值是互不影响的。
第二部分:数据结构常考知识
1、链表
1.单向链表
注:一般考察双链表的结构或者如何得到具体某个节点
如题:
单向链表中,只允许一次循环的前提下,如何得到倒数第k个节点的地址空间?
思路:
定义两个指针,第一个指针从链表的头指针开始遍历向前走k-1步,第二个指针保持不变;从第k步开始,第二个指针也开始从链表的头指针开始遍历,由于两个指针的距离保持在k-1,当第一个指针到达链表的尾节点时,第二个指针正好指向倒数第k个节点。
ListNode *FindKthTotail (ListNode* pListHead , unsigned int k)
{
ListNode* pAhead = pListHead;
ListNode* pBhead = pListHead;
for(unsigned int i=0;i<k-1 ;i++)
{
pAhead = pAhead->next;
}
pBhead = pListhead;
while(pAhead->m_pNULL != nullptr)
{
pAhead = pAhead->m_pNext;
pBhead = pBhead->m_pNext;
}
return pBhead;
}
2.双向链表
①双向链表的节点结构 例:
Typedf struct line {
struct line * prior; //指向前向指针
int data ; //结构体数据域
Struct line *next; //指向后继指针
}line;
②双向链表的建立
同单向链表相比,双链表仅是各节点多了一个指向前驱节点的指针域,每创建一个节点,都要与前驱节点建立两次联系:
第一个:新结点的prior 指向前驱节点。
第二个:前驱节点的next指针指向新结点。
建立双向链表代码如下:
Line *initline(Line *head)
{
head=(line *)malloc(sizeof(line)); //创建第一个头结点
head->prior = NULL;
head->date=1;
head->next=NULL;
Line *list = head; //声明指向头结点的指针,方便链表添加新的节点
for(int i=0 ; i<3 ; i++){
Line *body = line(line *)malloc(sizeof(line));
body->prior = NULL; //建立并且初始化一个新节点
body->next = NULL;
Body->date = 1;
List->next = body; //新结点与头结点建立联系
body->prior = list;
List = list->next; //List永远指向下一个节点
}
return head;
}
2、排序
1.冒泡排序
原理:
从左到右相邻元素进行比较,每比较一轮找到序列中最大或者最小的一个,这个数会从右边冒出来。
第一轮比较:所有数最大的会浮现到最右边
第二轮比较,所有数第二大的数会浮现到倒数第二个位置
有n个数据需要比较(n-1)轮,除第一轮外都不用比较全部
代码如下:
#include<stdio.h>
#include<string.h>
int main()
{
int a[]={22,62,1,-22,95,47,85,63,-45,152,19,28};
int n; //数组个数
int i;
int j; //需要进行比较的轮数
int buf; //用于冒泡交换
n=sizeof(a)/sizeof(a[0]);
for(i=0;i<n-1;++i)//进行n-1轮比较
{
for(j=0;j<n-i-1;++j)//每轮进行n-i-1次
{
if(a[j]>a[j+1])
{
buf=a[j];
a[j]=a[j+1];
a[j+1]=buf;
}
}
}
for(i=0;i<n;++i)
{
printf("%d\x20",a[i]);
}
printf("\n");
system("pause");
return 0;
}
2.选择排序
原理:
给定一个数组,设定一个临时变量用来存储最小值,将第一位数与后面的数字进行一一比较,有比第一个数小则交换位置,然后与其他数进行比较来选择是否交换,以此来找到第一位然后是第二位。
代码如下:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int i=0;
int j=0;
int temp=0;
int a[10]={10,62,-25,147,98,14,2,63,85,34};
for (i=0;i<10;i++)
{
for(j=i+1;j<10;j++)
{
if(a[i]>a[j])
{
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
}
for(i=0;i<10;i++)
{
printf("%d",a[i]);
printf(" ");
}
system("pause");
return 0;
}
3.插入排序
第三部分:Linux常考基础知识
1. Linux系统面试常考指令
(慢慢还在补充,正常也都不会怎么考)
①文件
- ls :显示当前文件的所有内容。
- cd :切换当前目录。
- pwd :显示当前工作路径。
- tree :显示文件和目录(由根目录开始的树形结构)。
- mkdir :创建目录。
- rm :删除文件。
- rmdir :删除目录。
- cp :复制文件/mul.
- touch :创建一个文件夹。
②其他
- find :文件寻找
- mount :挂载文件系统
- useradd :创建一个新用户
- cat :在命令行中显示文件的内容
- ps -aux | grep :显示当前的进程
- history :显示敲过的指令
- echo $ PATH: 获得当前环境变量
- file :查看文件属性(x86/arm)
2. Linux进程间通信
(管道,FIFO,消息队列,信号量,共享内存)
1.管道
管道,通常指无名管道,是UNIX系统IPC最古老的形式
①特点
- 半双工(即数据只能在一个方向上流动),具有固定的写端和读端
- 它只能用于具有亲缘关系的进程之间的(父子进程或兄弟进程之间)
- 万物皆文件,可以被看成是一种特殊的文件,可以使用普通的读些
write,read
等函数。但它不是普通文件,并不属于其他文件系统,只存于内存中。
②原型
#include <unistd.h>
Int pipe(int fd[2]);
//返回值,若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:
fd[0]
为读而打开,fd[1]
为写而打开,
如下图:
要关闭管道只需将这两个文件描述符关闭即可。
③例子:
单个进程中的管道几乎没有作用,调用pipe的进程接着调用fork,这样就创建了父进程和子进程直接的IPC通道。
2.FIFO
FIFIO,也称为命名管道,它是一种文件类型
①特点:
- FIFO可以在无关的进程之间交换数据,与无名管道不同。
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
②原型:
#include <sys/stat.h>
- //返回值:成功返回0,出错返回-1
int mkfifo (const char *pathname, mode_t mode );
③例子
FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清楚数据,而且“先进先出”。
3.消息队列
消息队列是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
①特点
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程,进程终于时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随即查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
②原型
#include<sys/msg.h>
//创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key ,int flag);
//添加消息:成功返回0,失败返回-1
int msgsnd(int msqid,const void *ptr,size_t size,int flag);
//读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr ,size_t size ,long type ,int flag);
//控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd ,struct msqid_ds *buf);
③例子
简单的使用消息队列进行IPC的例子,服务端程序等待特定类型的消息,当收到该类型的消息以后,发生另一种特定类型的消息作为反馈,客户端读取反馈并且打印。
代码:
Msg.server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/msg.h>
//用于创建一个唯一的key
#define MSG_FILE "/etc/passwd"
//消息结构
struct msg_form{
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 获取key值
if((key = ftok(MSG_FILE,'z')) <0 ){
perror("ftok error");
exit(1);
}
//打印key值
printf("Message Queue - Server key is :%d.\n", key);
//创建 消息队列
if(msgqid = msgget(key ,IPC_CREAT | 0777) == -1){
perror("msgget error");
exit(1);
}
//打印消息队列ID及进程ID
printf("My msqid is :%d.\n",msqid);
printf("My pid is :%d.\n",getpid());
//循环读取消息
for(;;){
msgrcv(msqid , &msg ,256 ,888 ,0); //返回类型为888的第一个消息
printf("Server: receive msg.mtext is:%s\n",msg.mtext);
printf("Server: receive msg.mtype is:%d\n",msg.mtype);
msg.mtype = 999; //客户端接收的消息类型
sprintf(msg.mtext,"hello,I'm server %d",getpid());
msgsnd(msqid,&msg, sizeof(msg.mtext), 0);
}
return 0;
}
Msg.client.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/msg.h>
//用于创建一个唯一的key
#define MSG_FILE "/etc/passwd"
//消息结构
struct msg_form{
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 获取key值
if((key = ftok(MSG_FILE,'z')) <0 ){
perror("ftok error");
exit(1);
}
//打印key值
printf("Message Queue - Client key is :%d.\n", key);
//打开消息队列
if(msgqid = msgget(key ,IPC_CREAT | 0777) == -1){
perror("msgget error");
exit(1);
}
//打印消息队列ID及进程ID
printf("My msqid is :%d.\n",msqid);
printf("My pid is :%d.\n",getpid());
//添加消息,类型888
msg.mtype = 888;
sprintf(msg.mtext,"hello,I'm client %d",getpid());
msgsnd(msqid,&msg,sizeof(msg.mtext),0);
//读取类型为777的消息
msgrcv(msqid,&msg,256,999,'0');
printf("client: receive msg.mtext is:%s\n",msg.mtext);
printf("client: receive msg.mtype is:%d\n",msg.mtype);
return 0;
}
4.信号量
信号量与已经介绍过的IPC结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
①特点
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
- 信号量基于擦做系统的PV操作,程序对信号量的操作都是原子操作。
- 每次信号量的PV操作不仅限于对信号量值加1或减1,而且可以加减任意正整数。
- 支持信号量组。
②原型
最简单的信号量是只能读取0和1的变量,这也是信号量最常见的一种形式,叫做二值信号量。而可以取多个正整数的信号量被称为通用信号量。
linux下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1,如果是引用一个现有的集合,则将Num_sems指定为0.
在semop函数中,sembuf结构定义如下:
5.共享内存
共享内存,指的领个或多个进程共享一个给定的存储区。
①特点
- 共享内存是最快的一种IPC(进程间通信),因为进程是直接对内存进行存取。
- 因为多个进程可以同时操作,所以需要进行同步。
- 信号量+共享内存通常结合在一起操作,信号量用来同步对共享内存的访问。
②原型
- 当用shmget函数创建一段共享内存时,必须指定其size;而如果引用一个已经存在的共享内存,则将size指为0.
- 当一段共享内存被创建猴,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存对象映射到调用的地址空间,随后可像本地空间一样访问。
- shmdt函数是用来断开shmat建立的连接。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。
- shmctl函数可以对共享内存执行多种操作,根据参数cmd执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。
进程间通信——五种通讯方式总结
- 管道:速度慢,容量有限,只有父子进程能通讯
- FIFO:任何进程间都能通讯,但速度慢
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
- 信号量;不能传递复杂消息,只能用来同步
- 共享内存: 能够很容易控制容量,速度快,但是要保持同步,比如一个进程在写的时候,另一个进程要注意读的问题,相当于线程中的线程安全。当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了用一进程的一块内存。
3. Linux进程(线程同步与互斥)
科普:linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态,linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景。
linux内核是多进程,多线程的操作系统,它提供了相当完整的内核同步方法。
内核同步方法如下:
①中断屏蔽
②原子操作
③自旋锁
④读写自旋锁
⑤顺序锁
⑥信号量
⑦读写信号量
⑧BKL(大内核锁)
⑨Seq锁
-
1.并发与竞态
定义:并发指的是多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问很容易导致竞态。
在linux中,主要的竞态发生在如下几种情况:
-
对称多处理器(SMP)多个CPU特别是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
-
单CPU进程与抢占它的进程。
-
中断(硬中断,软中断,Tasklet,低半步)与进程直接质押并发的多个执行单元存在对共享资源的访问,竞态就有可能发生如果中断处理程序访问进程正在访问的资源,则竟态也会发生。
多个中断直接本身也可能引起并发而导致竟态(中断被更高优先级的中断打断)。解决竟态问题的途径就是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。
访问共享资源的代码区域被称为临界区,临界区象牙以某种互斥机制加以保护,中断屏蔽,原子操作,自旋锁,和信号量都是Linux设备驱动中可采用的互斥途径。
临界区和竞争条件: 所谓临界区就是访问和操作共享数据的代码段,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行—也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样,如果两个执行线程有可能处于同一个临界区中,那么就是程序包含一个BUG,如果这种情况发生了,我们就称之为竞争条件,避免并发和防止竞争条件被称为同步。
死锁:
死锁的产生需要一定条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了,所以线程都在相互等待,但他们永远不会释放已经占有的资源,于是任何线程都无法继续,这便意味着死锁的发生。 -
2.中断屏蔽
在单CPU范围内避免竟态的一种简单的方法是在进入临界区之前屏蔽所有的中断。由于Linux内核的进程调度等操作都依靠中断来实现,内核抢占进程之间的并发也就得以避免了。中断屏蔽的使用方法:
①local_irq_disable() //屏蔽中断 //临界区
②local_irq_enable() //开中断特点: 由于Linux系统的异步IO,进程调度等很多重要操作都依赖于中断,在屏蔽中断期间所有的中断都无法得到处理,因此长时间的屏蔽是很危险的,有可能造成数据丢失甚至系统崩溃,这就要求在屏蔽中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
中断屏蔽只能禁止本CPU内的中断,因此,并不能解决多CPU引发的竟态,所以单独使用中断屏蔽并不是一个值得推荐的避免竟态的方法,它一般和自旋锁配合使用。 -
3.原子操作
定义: 原子操作指的是在执行过程中不会被别的代码路径所中断的操作, (原子原本指的是不可分割的微粒,所以原子操作也就是不能够被分割的指令) (它保证指令以”原子”的方式执行而不能被打断)原子操作是不可分割的,在执行完毕不会被任何其他任务或事件中断。在单处理器系统中,能够在单条指令中完成的操作都可以认为是”原子操作”,因为中断只能发生在指令之间。这也就是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。但是,在对称多处理器结构中就不同了,由于系统中有多个处理器在独立的运行,即使能在单条指令中完成的操作也有可能收到干扰。我们以decl(递减指令)为例,这是一个典型的”读-改-写”过程,涉及两次访问内存。
通俗易懂:
原子操作,顾名思义,就是说像燕子一样不可再细分。一个操作是原子操作,意思就是所这个操作是以原子的方式被执行,要一口气执行完,执行过程不能被其他OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其他行为是插不进来的。原子整数操作:
针对整数的原子操作只能对atomic_t类型的数据进行处理,在这里之所以引入了一个特殊的数据类型,而没有直接使用C语言的int型,主要处于两个问题:- 让原子函数只接受atoic_t类型的操作数,可以确保原子操作只与这种特殊类型数据一起使用,同时,这也确保了该类型的数据不会被传递给其他任何原子函数;
- 使用atomic_t类型确保编译器不对相应的值进行访问优化–这点使得原子操作最终接收到正确的内存地址,而不是一个别名,最后就是在不同体系结构上实现原子操作的时候,使用atomic_t可以屏蔽其间的差异。
原子操作最常见的用途就是实现计数器。
另一点需要说明原子操作只能保证操作是原子的,要么完成,要么不完成,不会有操作一半的可能,但原子操作并不能保证操作的顺序性,即它不能保证两个操作是按某个顺序完成的。如果要保证原子操作的顺序性,请使用内存屏障指令。在你编写代码的时候,能使用原子操作的时候,就尽量不要使用复杂的加锁机制,对多数体系结构来说,原子操作与更复杂的同步方法相比较,给系统带来的开销小,对高速缓存行的影响也小,但是,对于那些有高性能要求的代码,对多种同步方法进行测试比较,不失为一种明智的作法。
原子位操作:
针对位这一级数据进行操作的是函数,是对普通的内存地址进行操作。它的参数是一个指针和一个位号。
为了方便期间,内核还提供了一组与上述操作对应的非原子位操作,非原子位函数与原子位函数的操作完全相同,但是,前者不保证原子性,且其名字前缀多两个下划线。例如,与test_bit()对应的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已经用锁保护了你的数据),那么这些非原子的位函数相比位函数可能会执行得更快些。
4、 Linux进程和线程的区别
5、 buffer和cache的区别
第四部分. 网络通信应用
TCP、UDP协议的区别:
第五部分.ARM体系架构
附录(一些公司的笔试题)
更多推荐
所有评论(0)