C语言基础八股文

1、在1G物理内存的计算机中能否malloc(1.2G),为什么?

 是有可能申请成功1.2G内存的。

要点:malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与进程的虚拟地址空间相关。程序运行时,堆空间只是操作系统划分出来的一大块虚拟地址空间。应用程序通过malloc申请空间,得到的是堆空间的地址,之后程序运行所需要的实际物理内存是由操作系统映射完成的。

2、什么是栈区、堆区和静态区?

  • 栈区:存储局部变量(由CPU管理自动入栈和出栈,先进后出,连续的内存,每个线程有自己的栈区)。
  • 堆区:动态存储(非常大的内存池,非连续分配,malloc申请的内存就是从堆区分配的,空间分配和释放由程序员自己管理)。
  • 静态区:存储全局变量和静态变量,在程序的整个生命周期都存在。
    在这里插入图片描述

3、sizeof和strlen的区别是什么?

  • sizeof是一个单目运算符,而不是函数,字节数的运算在程序编译时进行,把sizeof的地方都替换成了具体的数字。sizeof返回变量或数据类型所占的内存字节数。如果传入的是指针,则所有的指针都是占用4个字节的空间,因此就返回4。如果是字符串则会统计 ‘\0’,数组则按照元素类型和数组长度来判断占用的字节数。
  • strlen是一个函数,仅对字符串有效,遇到 ‘\0’ 就结束,而不计入长度。

4、extern的作用是什么?

  • 可以用来声明变量和函数作为外部变量或者函数供其他.c文件调用;
  • c++里面调用c语言的接口,可以用extern来让c++编译器以c语言来编译此接口。
    #ifdef __cplusplus
    extern "C"{
    #endif
    
    #C接口声明
    
    #ifdef __cplusplus
    }
    #endif
    

5、malloc、calloc和realloc有什么区别?

  • malloc的调用形式为 (类型*)malloc(size):在内存的动态存储区中分配一块长度为size字节的连续区域,返回该区域的首地址。
  • calloc的调用形式为 (类型*)calloc(n, size):在内存的动态存储区中分配n块长度为size字节的连续区域,并且把分配的区域都初始化为0,返回该区域的首地址。
  • realloc的调用形式为 (类型*)realloc(ptr, size):将ptr内存大小调整为size字节(扩大或缩小)。
  1. malloc不对申请的内存空间进行初始化,而calloc会把申请到的内存空间初始化为0,所以malloc的效率相对较高一点。当malloc申请一段长度为0的空间时,依然会返回一段地址空间,而不是返回NULL(C99及之后的版本)。malloc函数会有一个阈值,申请小于这个阈值的空间,那么就会返回这个阈值大小的空间,并且这个阈值会随着编译器的不同而不同。如果是申请一个负数,那么返回的是NULL,因为malloc函数的参数类型是size_t,它是一个无符号整型。
  2. realloc在扩大内存空间时,如果当前内存块后面有足够的内存空间,则直接扩展这块内存空间,realloc返回原指针。如果当前内存块后面空闲内存不够,那么就使用堆中的第一个能满足大小要求的内存块,将当前数据拷贝过去,并且把当前的内存释放掉,返回新的内存块首地址。
  3. realloc返回的地址空间也不会进行初始化。

6、使用malloc申请空间和直接定义变量有什么区别?

  • 变量或数组的声明并没有分配内存,只是在使用的时候才分配内存。例如 int arr[64000] 需要很大的内存,如果系统中没有这么多的字节,使用 arr[index] 索引的时候就可能误操作其他的内存空间,导致无法预期的错误。
  • 生命周期不同,有些变量存在于多个函数或多个线程,因为局部变量是在栈空间中不能满足要求,而全局变量的生命周期是和程序的整个生命周期相同,无法自由释放。这种情况就可以使用malloc动态分配内存,可以自由申请和释放。
  • malloc可以申请一大块连续的内存,这一大块连续内存可以存储复杂的结构体和其他类型,还可以灵活指定地址偏移。

7、malloc的底层是怎么实现的?

Linux的虚拟内存管理几个关键概念:

  1. 在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页为单位。一个内存页是一段固定大小的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096(4K)字节;
  2. 每个进程都有独立的虚拟地址空间,进程访问的是虚拟地址并不是真正的物理地址;
  3. 虚拟地址可通过每个进程上的页表(在每个进程的内核虚拟地址空间)与物理地址进行映射,获得真正的物理地址;
  4. 如果虚拟地址对应物理地址不在物理内存中,则产生缺页中断,去真正分配物理地址,同时更新进程的页表。如果此时物理内存已耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中。

发生缺页中断后,执行了哪些操作?
当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:
1、检查要访问的虚拟地址是否合法
2、查找/分配一个物理页
3、填充物理页内容(读取磁盘或者直接置0,或者啥也不干)
4、建立映射关系(虚拟地址到物理地址)

  malloc()在运行时动态分配内存,用free()释放由其分配的内存。malloc()在分配用户传入的大小的时候,还分配一个相关的用于管理的额外内存。不过,用户是看不到的,malloc()返回的是申请到的用户空间的首地址:

分配的实际大小=管理空间+用户空间

  堆中的内存块总是成块分配的,并不是申请多少字节,就拿出多少字节的内存来提供使用。堆中内存块的大小通常与内存对齐有关(8Byte(for 32bit system)或16Byte(for 64bit system)。
因此,在64位系统下,当(申请内存大小+sizeof(struct mem_control_block) )% 16 == 0的时候,刚好可以完成一次满额的分配,但是当其!=0的时候,就会多分配内存块。
具体分配过程:

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk()和mmap()
1、brk()是将数据段(.data上面就是堆空间,两者紧挨着)的最高地址指针_edata往高地址推;
2、mmap()是在进程的虚拟地址空间中(堆和栈之间的文件映射区域)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。第一次访问已分配的虚拟地址空间的时候会发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc()/free()函数分配释放内存,这两个函数底层是由brk(),mmap(),munmap()这些系统调用实现的。

  1. malloc()分配的内存大小小于128k
      初始时,进程会有一个初始大小的堆空间。_enddata指向堆顶,通常通过链表或位图管理这些空闲内存。当需要分配的空间小于128k时,将在堆上分配对应内存空间。malloc函数首先遍历已管理的堆空间(_enddata指针此时指向的地址以下),若存在空闲的内存能满足所需大小,则分配该部分内存。当遍历所管理的所有空闲内存空间后发现没有能满足需要的,就调用系统调用函数brk()把_enddata往高地址推,即扩大堆空间以满足需求。
      调用malloc()时,只是分配了对应的虚拟地址空间,只有当读写该部分内存发生缺页中断时才会真正分配物理内存并将物理内存和虚拟内存建立映射关系,并且根据实际使用多少内存分配多少物理内存。通过这种策略大大提高了内存使用率。
      当调用free()释放上面所申请的内存时,free()函数会把传入的要释放的内存地址对应的内存控制块中的is_available字段置为1,表示释放。而且释放后的内存依然用链表或位图管理。

:此时该部分内存并未真正意义上回收,内核端认为该内存处于使用状态,对应的物理页仍然对应该部分虚拟内存映射(通常所说的内存碎片)。若此时产生了新的内存分配需求,而该部分内存能满足需求,则直接分配该部分内存。当释放该部分内存后,堆顶指针_enddata附近的连续空闲内存大于128k时,将进行真正意义上的内存回收操作。

  1. malloc()分配的内存大小大于128k
      当malloc()需要分配的内存空间大于128k时,将调用系统调用函数mmap()分配。此时分配的内存位置也和上面不一样,不再向上扩展_enddata指针了,而是直接在堆和栈之间的区域(文件映射区域)分配一块虚拟内存。当调用free()接口时,即调用系统调用函数munmap()直接释放该部分内存。

根据malloc()/free()调用实例进行说明。如下图所示:
在这里插入图片描述

  1. 初始进程虚拟内存空间分布如图1所示;
  2. 调用malloc()申请分配A:100k内存,堆顶指针_enddata上移,如图2所示;
  3. 调用malloc()申请分配B:200k内存,堆顶指针_enddata不动,底层调用mmap()系统调用函数在堆和栈之间的文件内存映射区直接分配200k内存,如图3所示;
  4. 调用malloc()申请分配C:40k内存,堆顶指针_enddata上移,如图4所示;
  5. 调用free()释放A:100k内存,由于申请释放的内存不在堆顶,因此堆顶指针_enddata不动,实际上没有真正意义上释放内存,该虚拟内存和实际物理内存映射关系仍然存在,形成了内存碎片。注意,此时若申请内存,且刚好小于100k,则可能把A释放出来的内存重新分配,提高内存利用率。如图5所示;
  6. 调用free()释放B:200k内存,堆顶指针_enddata不动,底层直接调用munmap()系统调用函数释放了该部分内存。如图6所示;
  7. 调用free()释放C:40k,由于申请释放的内存在堆顶,释放后A、C的空闲空间连续,且大小大于128k,因此,此时将会把A、C内存页释放,对应的映射关系取消,内存碎片消失,获得真正意义上的内存释放。

提问:既然堆内存碎片不能直接释放,导致疑似“内存泄露”问题,为什么malloc()不全部使用mmap()来实现呢(mmap()分配的内存可以通过munmap()进行释放)?而是仅仅对于大于128k的大块内存才使用mmap()?
答:其实,进程向系统申请和释放地址空间的接口brk()/mmap()/munmap()都是系统调用函数,频繁调用系统调用都比较消耗系统资源。并且,mmap()申请的内存被munmap()后,重新申请会产生更多的缺页中断。例如使用mmap()分配1M空间,第一次调用产生了大量缺页中断(1M/4K次),当munmap()后再次分配1M空间,会再次产生大量缺页中断。缺页中断是内核行为,会导致内核态CPU消耗较大。另外,如果使用mmap()分配小内存,会导致地址空间的分片更多,内核的管理负担更大。
  同时,堆是一个连续空间,并且堆内碎片由于没有归还给系统,如果可重用碎片,再次访问该内存很可能不需要产生任何系统调用和缺页中断,这将大大降低CPU的消耗。因此,glibc编译器的malloc()实现,充分考虑了brk()和mmap()行为上的差异及优缺点,默认分配大块内存才使用mmap()获得地址空间。
malloc()实现

/**内存控制块数据结构,用于管理所有的内存块
* is_available: 标志着该块是否可用。1表示可用,0表示不可用
* size: 该块的大小
**/
struct mem_control_block {
    int is_available;
    int size;
};

/**在实现malloc时要用到linux下的全局变量
*managed_memory_start:该指针指向进程的堆底,也就是堆中的第一个内存块
*last_valid_address:该指针指向进程的堆顶,也就是堆中最后一个内存块的末地址
**/
void *managed_memory_start;
void *last_valid_address;

/**malloc()功能是动态的分配一块满足参数要求的内存块
*numbytes:该参数表明要申请多大的内存空间
*返回值:函数执行结束后将返回满足参数要求的内存块首地址,要是没有分配成功则返回NULL
**/
void *malloc(size_t numbytes) {
    //游标,指向当前的内存块
    void *current_location;
    //保存当前内存块的内存控制结构
    struct mem_control_block *current_location_mcb;
    //保存满足条件的内存块的地址用于函数返回
    void *memory_location;
    memory_location = NULL;
    //计算内存块的实际大小,也就是函数参数指定的大小+内存控制块的大小
    numbytes = numbytes + sizeof(struct mem_control_block);
    //利用全局变量得到堆中的第一个内存块的地址
    current_location = managed_memory_start;

    //对堆中的内存块进行遍历,找合适的内存块
    while (current_location != last_valid_address) //检查是否遍历到堆顶了
    {
        //取得当前内存块的内存控制结构
        current_location_mcb = (struct mem_control_block*)current_location;
        //判断该块是否可用
        if (current_location_mcb->is_available)
            //检查该块大小是否满足
            if (current_location_mcb->size >= numbytes)
            {
                //满足的块将其标志为不可用
                current_location_mcb->is_available = 0;
                //得到该块的地址,结束遍历
                memory_location = current_location;
                break;
            }
        //取得下一个内存块
        current_location = current_location + current_location_mcb->size;
    }

    //在堆中已有的内存块中没有找到满足条件的内存块时执行下面的函数
    if (!memory_location)
    {
        //向操作系统申请新的内存块
        if (sbrk(numbytes) == -1)
            return NULL;//申请失败,说明系统没有可用内存
        memory_location = last_valid_address;
        last_valid_address = last_valid_address + numbytes;
        current_location_mcb = (struct mem_control_block)memory_location;
        current_location_mcb->is_available = 0;
        current_location_mcb->size = numbytes;
    }
    //到此已经得到所要的内存块,现在要做的是越过内存控制块返回内存块的首地址
    memory_location = memory_location + sizeof(struct mem_control_block);
    return memory_location;
}

free()实现

/**free()功能是将参数指向的内存块进行释放
*firstbyte:要释放的内存块首地址
*返回值:空
**/
void free(void *firstbyte)
{
    struct mem_control_block *mcb;
    //取得该块的内存控制块的首地址
    mcb = firstbyte - sizeof(struct mem_control_block);
    //将该块标志设为可用
    mcb->is_available = 1;
    return;
}

8、static有哪些用法?

  • 用static修饰局部变量:使局部变量存储在静态存储区,静态局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
  • 用static修饰全局变量:使其只在本文件内部有效,其他文件不可以链接或引用该变量。
  • 用static修饰函数:对函数的链接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数叫做静态函数,使用静态函数可以避免与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。

9、const有哪些用法?

  • 用const修饰变量:变量定义时就初始化,以后不能更改。
  • 用const修饰形参:该形参在函数内部不能被修改。
  • 用const修饰函数:返回值是常量不能被修改。
    被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

const int a;
int const a;
这两个声明的作用是一样的,a是一个常整形数

const int *a;
a是一个指向常整型数的指针(也就是,不能通过指针去修改整型数,但指针本身可以被修改)

int * const a;
a是一个指向整型数的常指针(也就是,可以通过指针去修改整型数,但指针本身不可以被修改)

const int * const a;
a是一个指向常整型数的常指针(也就是,不能通过指针去修改整型数,同时指针本身也不可以被修改))

10、volatile的作用和用法?

  一个定义为volatile的变量表示该变量会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。准确地说,优化器在用到这个变量时必须每次都会重新读取这个变量在内存中的值,而不是使用保存在寄存器里的备份(虽然读写寄存器比读写内存更快)。
以下几种情况都会用到volatile:

1、外围设备的特殊功能寄存器
对于嵌入式偏硬件方面的程序,经常要控制一些外围硬件设备,就拿I/O端口来说,需要去操作映射到对应I/O端口的寄存器。假设某一个寄存器的地址为0x1234,在C语言中,我们可以定义一个指针pRegister指向这个地址:

unsigned int *pRegister = (unsigned int *)0x1234;

实际应用中:我们经常会去判断一个寄存器中的值(或者寄存器中的某一位)为0还是1.例如下面程序:

unsigned int *pRegister = (unsigned int *)0x1234;  

//wait  
while(*pRegister == 0){
   //不改变*pRegister的值
}  

//Code...  

我们代码的目的是不断地判断 *pRegister的值是否为0,如果*pRegister的值(这个值由硬件改变)在中途变为1,则跳出死循环。
因为上面的循环中,*pRegister的值并没有发生改变,因为编译器会对上述代码进行优化,如下:

unsigned int *pRegister = (unsigned int *)0x1234;  

//wait  
if(*pRegister == 0){
   while(1){
       //不改变*pRegister的值
  }  
}

//Code... 

经过优化后,在上面的循环中,*pRegister的值不会发生改变,所以循环中就不再判断*pRegister的值了,运行效率提升。但是pRegister指向的特殊功能寄存器,其值是由硬件改变的,而软件却不再判断*pRegister的值了,那么就进入死循环了,即使*pRegister的值发生了改变,软件也察觉不到了。

2、在中断服务函数中修改全局变量

static int flag = 1;  

void main(void){  

while(flag == 1){  
    //code ...  
  }  
 //code ...  
}  

void do_interrupt(void){  //中断服务程序
  //code...  
  flag = 0;  
}  

中断程序中修改了全局变量,上面的代码简单,只要flag的值为1,就会一致运行循环里面的程序。因为flag值在循环里没有改变,编译器就将对其优化。如下:

static int flag = 1;  

void main(void){  

if(flag == 1){  
      while (1){  
          //code ...   
      }  
  }  
  //code ...   
}  

void do_interrupt(void){  
  //code...  
  flag = 0;  
}  

3、线程之间共享变量(在多线程中修改全局变量)

int  cnt;  

void task1(void){  
   cnt = 0;  
   while (cnt == 0) {  
       sleep(1);  
   }  
}  

void task2(void){  
   cnt++;  
   sleep(10);  
} 
同理对while进行了优化。

11、const常量和#define的区别是什么(编译阶段、安全性、内存占用等)?

  const用来定义常变量,占用存储单元,有类型;
  #define是宏定义语句,属于预编译指令,只是用符号常量代替一个字符串。
区别:

  1. 编译器处理方式不同
    define宏是在预处理阶段展开;
    const常量是在编译运行阶段使用。
  2. 类型和安全检查不同
    define宏没有类型,不做任何类型检查,仅仅是展开替换;
    const常量有具体的类型,在编译阶段会执行类型检查。
  3. 存储方式不同
    define宏仅仅是展开替换,有多少地方使用了宏就展开多少次,不会分配存储单元;
    const常量,系统会给其分配存储单元(可以是堆中也可以是栈中)。
  4. 系统内存占用不同
    对于const常量,系统只分配一个相应的内存地址,而define则是给出了一个立即数。因为const常量是存放在内存的静态区域中,所以在程序运行过程中const变量只有一个拷贝。而define所定义的宏却有多个拷贝,所以宏在程序运行过程中消耗的内存要比const常量大的多。

12、inline函数有什么作用?

  如果一些短小的函数被频繁的调用,不断地有函数入栈和出栈,会造成栈空间的大量消耗以及降低程序执行效率,这时就可以使用内联函数。在函数定义时使用 inline 关键字进行修饰可以把函数定义为内联函数,编译器会把内联函数内的代码直接插入到函数调用的位置,减少了函数调用的开销。内联函数和宏是有区别的,内联函数是在编译时展开,而宏是在预处理阶段展开。内联函数有类型检查和作用域限制,它更像是一种函数调用形式的宏。

13、怎么判断大小端?

  • 小端模式:高位数据存储在高位,低位数据存储在低位
  • 大端模式:高位数据存储在低位,低位数据存储在高位
//使用联合体的特性进行判断
#include <stdio.h>

typedef union
{
	int a;
	char b;
}test;

int main()
{
	test t;
	t.a = 0x12345678;
	if(0x78 == t.b)
	{
		printf("小端模式\n");
	}
	else
	{
		printf("大端模式\n");
	}
	return 0;
}
//使用指针类型转换来进行判断
#include <stdio.h>

int main()
{
	int a = 0x12345678;
	char *b = (char *)&a;
	
	if(0x78 == *b)
	{
		printf("小端模式\n");
	}
	else
	{
		printf("大端模式\n");
	}
	return 0;
}

14、指针函数和函数指针有什么区别?

  • 指针函数:本质上是一个函数,只是函数的返回值是一个指针,例:int *func(int a, int b)
  • 函数指针:本质上是一个指针,该指针的地址指向了一个函数,例:int (*func)(int a, int b)

函数指针常用于回调函数,可以使用 typedef 定义一种函数指针的类型,例typedef int (*func)(int a, int b),然后定义一个回调函数:

static func pFunc = NULL;
void funcCallBackInit(func pFuncCallBack)
{
	if(NULL != pFuncCallBack)
	{
		pFunc = pFuncCallBack;
	}
}

例如在开发中有些函数需要在单板层实现,但是在平台下调用,这样可以在不同的单板下实现差异化。这时就用到了回调函数,即在平台定义一个回调函数,通过在单板层调用回调函数把单板层的函数传给平台层去使用。

15、void *类型是什么?

任何数据都是有类型的,告诉编译器分配多少内存。
void val; //错误的
但是void *可以,四个字节
void *是所有类型指针的祖宗

int *pInt = NULL;
char *pChar = pInt;

int *赋值到char *系统会告警,可以使用类型转换

int *pInt = NULL;
char *pChar = (char *)pInt;

或者用void *接收

void *pVoid = pInt;

任何类型的指针,都可以不经过强制转换,直接赋值给void *类型,所以可以理解为void *是所有类型指针的祖宗。

  • void *主要用于数据结构的封装。
  • 在取指针的内容的时候,比如取void *类型是不可以取内容的,因为未知目标类型。
  • void *可以作为函数的返回值,函数中接收void *返回值类型的函数返回的值时,需要对数据进行强制类型转换。

16、实现一个strcpy函数,解释为什么要返回char *

已知strcpy函数的原型是:
char *strcpy(char *strDest, char *strSrc);
代码实现:

char *strcpy(char *strDest, char *strSrc)
{
	if((NULL == strDest) || (NULL == strSrc))
	{
		throw "Invalid argument(s)";
	}
	char *strDestCopy = strDest;
	while((*strDest++ = *strSrc++) != '\0');
	return strDestCopy;
}

strcpy能把strSrc的内容复制到strDest,为什么还要char *类型的返回值?
返回strDest的原始值使函数能支持链式表达式。
链式表达式的形式如下:
int iLength = strlen(strcpy(strA, strB));
或者是:
char *strA = strcpy(new char[10], strB);

注意:拷贝的时候源地址和目的地址不能重叠,如有重叠满足特定条件时可以采用从后往前复制的方法。

17、手写memcpy,同时memcpy与strcpy的区别是什么?

  将由src指向地址为起始地址的连续n个字节的数据复制到以dest指向地址为起始地址的空间内,函数返回一个指向dest的指针。

注意

  1. src和dest所指内存区域不能重叠,所以可以使用restrict关键字修饰;
  2. 与strcpy相比,memcpy遇到’\0’并不会结束,而是一定会拷贝完n个字节;
  3. memcpy可以拷贝任何数据类型的对象,可以指定拷贝的数据长度;
  4. 如果dest本身就有数据,执行memcpy后会覆盖原有的数据;
  5. dest和src都不一定是数组,任意的可读写的空间都可以;
  6. 如果要追加数据,则每次执行memcpy后,要将目标数据地址增加到所要追加数据的地址。

restrict关键字:
该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于该指针,即不存在其他进行修改操作的途径。
void *memcpy(void * restrict dest, const void * restrict src, size_t n);

:这是一个很有用的内存复制函数,由于两个参数都增加了restrict限定,所以两块区域不能重叠,即dest指针所指的区域不能通过别的指针来修改。而且src通过const进行修饰,即src的指针不能修改。相对应的另一个函数 memmove(void *dest, const void *src, size_t n) 则可以重叠。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void * my_memcpy(void * restrict dest, const void * restrict src, unsigned int count)
{
	if(dest == NULL || src == NULL)
	{
		return NULL;
	}
	char *pdest = (char *)dest;
	char *psrc = (char *)src;
	while(count--)
	{
		*pdest++ = *psrc++;
	}
	return dest;
}

int main()
{
	char src[] = "hello";
	char dest[] = "world";
	my_memcpy(dest, src, strlen(src));
	printf("%s\n", dest);
	return 0;
}

18、C语言中的#和##的作用是什么,有什么区别?

  在C语言中,#是一个预处理器运算符,用于将宏参数转换为字符串常量。而##是另一个预处理器运算符,用于将两个标记(token)拼接在一起。

区别如下:

  • #操作符将宏参数作为字符串常量进行处理。例如,如果有一个宏定义#define STR(x) #x,那么STR(test)将被替换为"test"。这在输出调试信息或日志时非常有用。
  • ##操作符用于将两个标记(token)拼接在一起来创建新的标记。它可以将宏参数与其他文本进行连接。例如,如果有一个宏定义#define CONCAT(x, y) x##y,那么CONCAT(hello, world)将被替换为helloworld。这在创建通用的宏定义时非常有用。
    综上所述,#用于将宏参数转换为字符串常量,而##用于将两个标记拼接在一起。

19、什么是不可重入函数?

 不可重入函数是指在函数执行过程中,如果再次调用该函数,就会导致错误或不可预测的结果。这种函数一般会使用全局变量或静态变量来保存临时数据,导致函数执行过程中的状态被修改,进而影响下一次调用。
不可重入函数在多线程编程中是一个重要的问题,因为多个线程可能同时调用该函数,而且可能会共享全局变量或静态变量,从而导致竞争条件和错误的结果。为了避免这种情况,通常需要使用互斥锁或其他同步机制来保护不可重入函数。
相反,可重入函数是指可以被多个线程同时调用而不会出现竞争条件或错误结果的函数。可重入函数不使用全局变量或静态变量保存临时数据,而是使用函数的参数和局部变量来保存状态,从而保证每个线程都有自己的状态。

20、如何写出可重入函数?

  • 避免使用全局变量:全局变量的使用会导致函数不可重入,因为多次调用该函数可能会修改全局变量的值,而从导致逻辑错误。如果需要使用全局变量,可以采用局部静态变量或者函数参数来替代全局变量的功能。
  • 避免使用静态局部变量:静态局部变量会导致函数不可重入,因为多次调用该函数时,静态局部变量的值会被保留下来,从而导致逻辑错误。如果需要使用静态局部变量,可以考虑使用函数参数来传递状态。
  • 使用局部变量:在函数中使用局部变量可以确保不同函数调用之间的数据不会互相干扰,从而实现函数的可重入性。
  • 避免使用非数据安全的函数和数据结构:在函数中避免使用非线程安全的函数和数据结构,如全局变量、静态变量、共享内存等。可以使用线程安全的替代方案,如使用局部变量、互斥锁等来保护共享资源的访问。
  • 注意函数的嵌套调用:在函数中调用其他函数时要注意函数的嵌套调用顺序,确保函数调用的顺序是正确的,避免函数调用的逻辑错误。

21、封装对寄存器进行位操作的函数,把某一位置1或清除。

#include <stdio.h>

#define SET_BIT(n)    (1UL << (n))
#define BIT_REV(n)    (~(1ULL << (n)))

int SetBit(int val, unsigned int n)
{
	return val | SET_BIT(n);
}

int ClearBit(int val, unsigned int n)
{
	return val & BIT_REV(n);
}

int main()
{
	int a = 3;
	a = SetBit(a, 2);
	printf("SetBit: a=%d\n", a);

	a = ClearBit(a, 2);
	printf("ClearBit: a=%d\n", a);
	return 0;
}

22、GCC编译过程?

  • 预处理/预编译:主要用于包含头文件的扩展,以及执行宏替换等 //加上 -E
  • 编译:主要用于将高级语言程序翻译成汇编语言 //加上 -S
  • 汇编:主要用于将汇编语言翻译成机器指令,得到目标文件 //加上 -c
  • 链接:主要用于将目标文件和标准库链接,得到可执行文件 //加上 -o

23、栈溢出的原因及解决方法?

原因:
1、局部数组变量空间太大;
2、函数出现无限递归调用或者递归层次太深。
解决方法:
1、动态内存分配
2、增大栈空间
3、优化递归调用

Linux部分

1、Linux常用指令

find命令用法
使用格式:find [指定查找目录] [查找规则] [查找完后执行的action]
1)根据文件名查找
 -name 根据文件名查找(精确查找)
 -iname 根据文件名查找,但是不区分大小写
文件名通配:
* 表示通配任意的字符
? 表示通配任意的单个字符
[] 表示通配括号里面的任意一个字符
find /etc -name “passwd*”
find /etc -name “passwd?”
find /home/test -name “[ab].sh”
~~
2)根据文件所属用户和组来查找文件
 -user 根据所属用户来查找文件
 -group 根据所属组来查找文件
find ./ -user root
find ./ -group root
~~
3)运算关系
-a 连接两个不同的条件(两个条件必须同时满足)
find ./ -name “*.sh” -a -user root

1、mmap底层实现原理是什么样的?

  1. mmap内存映射
    在unix/linux平台下读写文件,一般有两种方式。第一种是首先open文件,接着使用read系统调用来读取文件的全部或一部分。于是内核将文件的内容从磁盘上读取到内核页高速缓存,再从内核高速缓存读取到用户进程的地址空间。这么做需要在内核和用户空间之间做多次数据拷贝。而且当多个进程同时读取一个文件时,则每一个进程在自己的地址空间都有这个文件的副本,这样也造成了物理内存的浪费。如下图所示:
    在这里插入图片描述
    第二种方式是使用内存映射。mmap是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用readwrite等系统调用函数。相反,内核空间对这段区域的修改也直接反应到用户空间,从而可以实现不同进程间的文件共享。如下图所示:
    在这里插入图片描述
    从上图可以看出,进程的虚拟地址空间由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
    linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:
    在这里插入图片描述
    vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_ops指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用到的信息都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。
  2. mmap内存映射实现过程
    mmap内存映射的实现过程总的来说可以分为三个阶段:
  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
    (1)进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
    (2)在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址;
    (3)为此虚拟空间分配一个vm_area_struct结构,接着对这个结构的各个字段进行初始化;
    (4)把新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中。

  • 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
    (5)为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符链接到内核“已打开文件集”中该文件的文件结构(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
    (6)通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *file, struct vm_area_struct *vma),不同于用户空间函数。
    (7)内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
    (8)通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到物理内存中。

  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存的拷贝

    注:前两个阶段仅在于创建虚拟区域并完成映射,但是并没有将任何的文件数据拷贝到物理内存中。真正的文件读取是当进程发起读或写操作时。

    (9)进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到物理内存中,因此引发缺页异常。
    (10)缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
    (11)调页过程先在交换缓存空间中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到物理内存中。
    (12)之后进程即可对这段内存进行读或写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

    注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步,这样所写的内容就能立即保存到文件里了。

  1. mmap通过/dev/mem映射物理内存
    /dev/mem是linux下的一个字符设备,源文件是kernel/drivers/char/mem.c,这个设备文件是专门用来读写物理地址用的。里面的内容是所有物理内存的地址以及内容信息。通常只有root用户对其有读写权限。也就是说,使用mmap时,通过/dev/mem做了一个巧妙的转换,原本填写文件句柄的参数,只需要填上open /dev/mem之后的文件句柄,就可以直接完成对物理内存的映射。
    mmap函数入参说明如下:
    void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
    参数说明:
    start:映射区的开始地址
    length:映射区的长度
    prot:期望的内存保护标志
    —-PROT_EXEC //页内容可以被执行
    —-PROT_READ //页内容可以被读取
    —-PROT_WRITE //页可以被写入
    —-PROT_NONE //页不可访问
    flags:指定映射对象的类型
    —-MAP_FIXED
    —-MAP_SHARED 与其它所有映射这个对象的进程共享映射空间
    —-MAP_PRIVATE 建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件
    —-MAP_ANONYMOUS 匿名映射,映射区不与任何文件关联
    fd:如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
    offset:被映射对象内容的起点

CPU要访问该PCIE设备空间,只需访问对应的内存空间.

检查内存地址,如果发现该内存空间地址是某个PCIe设备空间的映射,就会触发其产生TLP,去访问对应的PCIe设备,读取或者写入PCIe设备

2、自旋锁和信号量有什么区别?

  1. 自旋锁
    自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
    自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已经被持有的自旋锁,那么这个任务就会一直进行忙循环—>旋转—>等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
    事实上,自旋锁的初衷就是:在短时间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自选(特别浪费处理器的时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话,最好使用信号量。
    自旋锁的基本形式如下:
    spin_lock(&mr_lock);
    //临界区
    spin_unlock(&mr_lock);
    
    因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理器需要的锁定服务。在单处理器上,自旋锁仅仅当做一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除内核。
    简单地说,自旋锁在内核中主要用来防止多处理器并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成死锁----因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁)。它能够在中断上下文中使用
    死锁:假设有两个内核任务和两个资源,每个内核任务都各自持有了一个资源并且等待请求已被对方持有的另一个资源,这便会造成所有的内核任务都互相等待,但它们永远不会释放自己已经占用的资源,于是导致任何内核任务都无法继续运行,这就是发生了死锁。
  2. 信号量
    Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其他代码。当持有信号量的进程把信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
    信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况,只能在进程上下文中使用,因为中断上下文中是不能被调度的。另外当代码持有信号量时,不可以再持有自旋锁。
    信号量的基本形式如下:
    static DECLARE_MUTEX(mr_sem); //声明互斥信号量
    if(down_interruptible(&mr_sem))
    //可被中断的睡眠,当信号来到,睡眠的任务被唤醒
    //临界区
    up(&mr_sem);
    
    信号量和自旋锁的区别:
    如果代码需要睡眠,使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更简单一些。如果需要在自旋锁和信号量中做选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该被尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选择。另外信号量不同于自旋锁,它不会关闭内核抢占,所以已经被持有的信号量可以被抢占。这意味着信号量不会对内核调度响应时间带来负面影响。

3、互斥锁和自旋锁有什么区别?

  1. 区别

    1. 实现方式上的区别:互斥锁是基于自旋锁实现的,所以自旋锁相较于互斥锁更底层。
    2. 开销上的区别:获取不到互斥锁时会发生上下文切换并休眠,而自旋锁则“自旋”在原地等待直到获取到锁。
    3. 使用场景的区别:互斥锁只能在进线程中使用,不能在中断里使用,而自旋锁可以在中断里使用。
    4. 使用方式上的区别:互斥锁只能由获取到该锁的进线程来释放,而自旋锁没有这个限制,但上锁和解锁一般都是成对使用的。
  2. 互斥锁
    互斥锁是一种独占锁,当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B就会加锁失败,同时释放CPU给其他线程,线程B加锁的代码就会被阻塞。
    互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。
    在这里插入图片描述
    互斥锁加锁失败后,会从用户态陷入内核态,让内核帮助我们切换线程,虽然简化了使用锁的难度,但是存在一定的性能开销成本。

    性能开销成本:两次线程上下文切换的成本。

    1. 当线程加锁失败时,内核将线程的状态从【运行】切换到【睡眠】状态,然后把CPU切换 到其他线程运行;
    2. 当锁被释放时,之前睡眠状态的线程会变成就绪状态,然后内核就会在合适的时间把CPU切换给该线程运行。

    线程切换的上下文:
    当两个线程属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
    上下文切换的耗时大概在几十纳秒到几微秒之间,如果锁住的代码执行时间比较短,可能上下文切换的时间比锁住的代码执行时间还要长。
    若是能耐确定临界区的代码执行时间很短,就不应该使用互斥锁,而应该选择自旋锁。

  3. 自旋锁
    自旋锁是在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些开销小一些。加锁失败的线程会占用CPU资源原地自旋等待,直到拿到锁。

    自旋锁利用COU周期一直自旋直到锁可用。由于一个自旋的线程永远不会主动放弃CPU,因此在单核CPU上,需要抢占式的调度器(不断通过时钟中断一个线程,运行其他线程)。
    自旋的时间和临界区的代码执行时间成正比关系。

4、字符设备和块设备的区别?

  • 字符设备
    字符设备是个能以字节流的方式进行读写、一个字节一个字节读写的设备(有先后顺序),由字符设备驱动程序来实现这种特性,未经过文件系统,读写速度快。字符设备驱动程序通常至少实现open、close、read和write系统调用函数,每个字符设备都在/dev目录下存在一个设备文件。字符终端、串口、鼠标、键盘、摄像头、声卡和显卡等就是典型的字符设备。
  • 块设备
    块设备以数据块的形式进行操作,能够随机存取数据。操作块设备往往通过文件系统,而不是直接与设备进行通讯。块设备的I/O操作方式与字符设备存在较大的不同,引入了request_queue、request、bio等一系列数据结构。在整个块设备的I/O操作中,贯穿始终的就是“请求”,字符设备的I/O操作则是直接进行,不需要绕弯,而块设备的I/O操作会排队和整合。U盘、SD卡、磁盘等就是块设备。

5、内核程序申请内存使用什么函数?和应用程序申请内存有什么区别?

内核中申请内存空间用的函数是kmalloc()、kzalloc()、vmalloc(),而应用程序中申请内存使用的是malloc()。

  1. kmalloc()/kzalloc()分配的虚拟地址和物理地址都是连续的,最大只能分配128kb的空间,并且分配的过程可以是原子过程。此外,kzalloc()还可以把分配的内存全部初始化为0。
  2. vmalloc()分配的是连续的虚拟地址,但物理地址不一定连续。而且这个物理内存映射建立的页表项只写入了内核页表中,并没有将更新的内核页表同步到用户进程的页表中,也就是lazy机制。当访问这段内存时,触发do_page_fault(缺页异常)才会把更新的内核页表同步到用户进程的页表中。此外,vmalloc()可能睡眠,不能从中断上下文中调用。
  3. malloc()是用户空间申请内存的方法,分配堆空间中连续的虚拟地址,物理地址不一定连续。在分配时并没有做实际物理页的分配动作,实际分配物理页的动作是在do_page_fault异常处理中完成的。

6、linux虚拟地址空间是如何分布的?

  • 每个进程都有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址;
  • 虚拟地址可通过每个进程上的页表(在每个进程的内核虚拟地址空间)与物理地址进行映射,获得真正物理地址;
  • 如果虚拟地址对应物理地址不做物理内存中,则产生缺页中断,真正分配物理地址,同时更新进程的页表;如果此时物理内存已经耗尽,则根据内存替换算法淘汰部分页面,把这些页面的内容写回物理磁盘中,并释放这些页面对应的物理内存。

Linux使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为:

  1. 只读段:该部分空间只能读,不可以写;(包括:代码段、rodata段(C字符串常量、#define定义的常量和const常量))
  2. 数据段:保存全局变量、静态变量;
  3. :就是平常所说的动态内存,malloc/new大部分来源于此,其中堆顶的位置可通过函数brk和sbrk进行动态调整;
  4. 文件映射区:如动态库、共享内存等映射物理空间的内存,一般是mmap函数所分配的虚拟地址空间;
  5. :用于维护函数调用的上下文空间,一般为8M,可通过ulimit -s查看;
  6. 内核虚拟空间:用户代码不可见的内存区域,由内核管理(页表就存在内核虚拟空间)。
    下图是32位系统典型的虚拟地址空间分布:
    在这里插入图片描述
    32位的Linux系统虚拟地址空间为4G,这4G的空间被分为两部分,最高的1G空间(从虚拟地址0xC0000000 ~ 0xFFFFFFFF)供内核使用,也即内核空间;较低的3G空间(从虚拟地址0x00000000 ~ 0xBFFFFFFF)供各个进程使用,也即用户空间。

7、ioremap()函数如何使用?

一、基本概念
在linux驱动中不可避免的会涉及操作外设,而外设的地址空间与DDR的地址空间一般不连续,在linux上电时,并不会为外设地址空间建立页表,又因为linux访问内存使用的都是虚拟地址,因此如果想访问外设的寄存器(通常包括数据寄存器、控制寄存器和状态寄存器),需要在驱动初始化中将外设所处的已知的物理地址映射为虚拟地址,然后根据映射所得到的内核虚拟地址范围,通过线性偏移(vir_addr = vir_base + phy_addr - phy_base)访问这些IO内存资源。
1、ioremap函数
ioremap宏定义在asm/io.h内:
#define ioremap(cookie, size)    __ioremap(cookie, size, 0)
__ioremap函数原型为(arm/mm/ioremap.c):
void __iomem *__ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
参数:
phys_addr: 要映射的起始IO地址
size: 要映射的空间大小
flags: 要映射的IO空间和权限有关的标志
该函数返回映射后的内核虚拟地址(在3G~4G上),然后便可以通过读写该返回的内核虚拟地址去访问这段IO内存资源。
2、iounmap函数
iounmap函数用于取消ioremap所做的映射,原型如下:
void iounmap(void *addr);
二、ioremap()相关函数解析
在将I/O内存资源的物理地址映射到内核虚拟地址后,理论上讲我们就可以像读写RAM那样直接读写I/O内存资源了。为了保证驱动程序跨平台的可移植性,我们应该使用linux中特定的函数来访问I/O内存资源,而不应该通过指向内核虚拟地址的指针来访问。
读写I/O的函数如下所示:
writel()
writel()往内存映射的I/O空间上写入32位数据(4字节)
原型:void writel(unsigned char data, unsigned short addr);
readl()
readl()从内存映射的I/O空间上读取32位数据(4字节)
原型:unsigned char readl(unsigned int addr);
源代码定义为:

#define readb __raw_readb
#define readw(addr) __le16_to_cpu(__raw_readw(addr))
#define readl(addr) __le32_to_cpu(__raw_readl(addr))
#ifndef __raw_readb
static inline u8 __raw_readb(const volatile void __iomem *addr)
{
    return *(const volatile u8 __force *) addr;
}
#endif
 
#ifndef __raw_readw
static inline u16 __raw_readw(const volatile void __iomem *addr)
{
    return *(const volatile u16 __force *) addr;
}
#endif
 
#ifndef __raw_readl
static inline u32 __raw_readl(const volatile void __iomem *addr)
{
    return *(const volatile u32 __force *) addr;
}
#endif
 
#define writeb __raw_writeb
#define writew(b,addr) __raw_writew(__cpu_to_le16(b),addr)
#define writel(b,addr) __raw_writel(__cpu_to_le32(b),addr)

三、实际案例
1、先定义一些寄存器,并且寄存器的地址均为物理地址

#define GPD0CON       0x114000a0  
#define TIMER_BASE    0x139D0000             
#define TCFG0         0x0000                 
#define TCFG1         0x0004                              
#define TCON          0x0008               
#define TCNTB0        0x000C            
#define TCMPB0        0x0010 

2、为了使用内存映射,需要先定义指针用来保存内存映射后的地址

static unsigned int *gpd0con;  
static void *timer_base;  

3、使用ioremap()函数进行内存映射,并将映射的地址赋给刚才定义的指针

gpd0con = ioremap(GPD0CON,4);  
timer_base = ioremap(TIMER_BASE,0x14); 

4、得到映射地址后,可以调用writel()、readl()函数进行相应的操作

writel ((readl(gpd0con)&~(0xf<<0)) | (0x2<<0),gpd0con);  
writel ((readl(timer_base +TCFG0  )&~(0xff<<0)) | (0xff <<0),timer_base +TCFG0);   
writel ((readl(timer_base +TCFG1 )&~(0xf<<0)) | (0x2 <<0),timer_base +TCFG1 );   
  
writel (500, timer_base +TCNTB0  );  
writel (250, timer_base +TCMPB0 );  
writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x2 <<0),timer_base +TCON );   

中断相关

  操作系统收到了中断请求,会打断其他进程的运行,所以中断请求的相应程序,也就是中断处理程序,要尽可能快的执行完,这样可以减少对正常进程运行调度的影响。
而且,中断处理程序在相应中断时,可能还会临时关闭中断,这意味着,如果当前中断处理程序没有执行完之前,系统中其他的中断请求都无法被相应,也就是说中断有可能会丢失,所以中断处理程序要短且快。

1、硬中断与软中断的区别?

  1. 硬中断
    • 硬中断是由硬件产生的,比如像磁盘、网卡、键盘、时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上(硬件驱动通常是内核中的一个子程序,而不是一个独立进程)。
    • 处理中断的驱动是需要运行在CPU上的,因此,当中断产生的时候,CPU会中断当前正在运行的任务,来处理中断。在有多核心的系统上,一个中断通常只能中断一颗CPU(也有一种特殊的情况,就是在大型主机上是有硬件通道的,它可以在没有主CPU的支持下,同时处理多个中断)。
    • 硬中断可以直接中断CPU,它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。
    • 对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程俩运行。它的存在就是为了让调度器调度多任务。
  2. 软中断
    • 软中断是一种在操作系统中用于处理异常情况或执行特定的操作的一种机制。它是由操作系统软件触发的中断,与硬件中断不同,软中断是由操作系统内核自身产生的。软中断可以用于实现系统调用、进程间通信、异常处理和时钟中断等功能。
    • 由操作系统内核主动触发,可以在任何时候被处理。
    • 不需要硬件干预,可以在用户态下执行。
    • 由操作系统自行定义和处理,具有灵活性和可扩展性。
    • 软中断通常通过中断向量表(Interrupt Vector Table,简称IVT)来实现,操作系统会为每一种软中断预分配一个固定的中断号。当发生软中断时,操作系统会根据中断号在IVT中查找相应的中断处理程序来进行处理。软中断的处理程序通常由操作系统内核编写,用于执行特定的操作或处理异常情况。

3、软中断所经过的操作流程是比硬中断的少吗?

  • 软中断
    进程->内核中的设备驱动程序
  • 硬中断
    硬件->CPU->内核中的设备驱动程序
    软中断比硬中断少了一个硬件发送信号的步骤。产生软中断的进程一定是当前正在运行的进程,因此它们不会中断CPU,但是它们会中断调用代码的流程。
    如果硬件需要CPU去做一些事情,那么这个硬件会使CPU中断当前正在运行的代码。而后CPU会将当前正在运行进程的当前状态放到堆栈中,以至于之后可以返回继续运行。这种中断可以停止一个正在运行的进程;可以停止正处理另一个中断的内核代码;或者可以停止空闲进程。

I2C知识:
https://blog.csdn.net/weixin_42031299/article/details/123602636?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170920708116800180641619%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170920708116800180641619&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-2-123602636-null-null.142v99pc_search_result_base4&utm_term=i2c&spm=1018.2226.3001.4187

  • 12
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
嵌入式C语言八股文是指在嵌入式系统开发中常见的基本知识点和技能要求的简要总结。下面是嵌入式C语言八股文的主要内容: 1. 数据类型:包括基本数据类型(如int、char、float等)和派生数据类型(如数组、结构体、枚举等),掌握各种数据类型的使用方法和特点。 2. 运算符:熟悉各种算术运算符、逻辑运算符、位运算符等,掌握它们的优先级和结合性,能够正确使用运算符完成各种计算任务。 3. 控制语句:包括条件语句(if-else语句)、循环语句(for、while、do-while循环)、选择语句(switch-case语句)等,掌握这些语句的使用方法和注意事项。 4. 函数:了解函数的定义和调用,能够编写函数并正确使用函数参数和返回值,理解函数的作用域和生命周期。 5. 数组和指针:掌握数组和指针的定义和使用,了解数组和指针在内存中的存储方式,能够通过指针进行数组的访问和操作。 6. 文件操作:了解文件操作的基本流程,包括文件的打开、读写和关闭,理解文件指针和文件访问模式的概念。 7. 中断处理:了解中断的基本概念和原理,能够编写中断服务程序(ISR)并正确处理中断请求。 8. 程序调试:掌握常用的调试技巧和工具,能够使用调试器进行程序的单步执行、观察变量值等操作,能够分析程序运行过程中的错误和异常。 以上是嵌入式C语言八股文的主要内容,掌握这些知识和技能,可以帮助你在嵌入式系统开发中更好地应对各种任务和挑战。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值