首页
chatGPT
关于
友链
其它
统计
更多
壁纸
留言
Search
1
cgroup--(4)cgroup v1和cgroup v2的详细介绍
6,536 阅读
2
修改Linux Kernel defconfig的标准方法
6,450 阅读
3
Android系统之VINTF(1)manifests&compatibility matrices
6,054 阅读
4
使用git生成patch和应用patch
3,570 阅读
5
c语言的__attribute__
3,184 阅读
默认分类
文章收集
学习总结
算法
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
Rust
工具
软件工具
Bug
COMPANY
登录
Search
标签搜索
Rust
shell
Linux
c
uboot
Vim
vintf
Linux驱动
Android
device_tree
git
DEBUG
arm64
链表
数据结构
IDR
内核
ELF
gcc
ARM
adtxl
累计撰写
380
篇文章
累计收到
16
条评论
首页
栏目
默认分类
文章收集
学习总结
算法
环境配置
知识点
入门系列
vim
shell
Git
Make
Android
Linux
Linux命令
内存管理
Linux驱动
Language
C++
C
Rust
工具
软件工具
Bug
COMPANY
页面
chatGPT
关于
友链
其它
统计
壁纸
留言
搜索到
380
篇与
的结果
2020-10-09
线程与对称处理器
1. 进程和线程进程的概念有两个特点,一是资源所有权。一个进程包括一个存放进程映像的虚拟地址空间;二是调度/执行。一个进程沿着通过一个或多个程序的一条执行路径(轨迹)执行。这两个特点是独立的,为了区分这两个特点,分派的单位通常称作线程,而拥有资源所有权的单位称为进程。1.1 多线程在多线程环境中,进程被定义成资源分配的单位和一个被保护的单位,与进程相关联的有:存放进程映像的虚拟地址空间受保护地对处理器、其他进程(用于进程间通信)、文件和I/O资源的访问。在一个进程中,可能有一个或多个线程,每个线程有:线程执行状态(运行、就绪等)在未运行时保存的线程上下文;从某种意义上看,线程可以被看做进程内的一个被独立地操作的程序计数器一个执行栈用于每个线程局部变量的静态存储空间与进程内的其他线程共享的对进程的内存和资源的访问。线程的优点:在一个已有进程中创建一个新线程比创建一个全新进程所需的时间要少许多。研究表明,在UNIX中,线程的创建比进程快10倍终止一个线程比终止一个进程花费的时间少同一个进程内线程间切换比进程间切换花费的时间少线程提高了不同的执行程序间通信的效率。在大多数操作系统中,独立进程间的通信需要内核的介入,以提供保护和通信所需要的机制。但是,由于同一个进程中的线程共享内存和文件,它们无需调用内核就可以互相通信。
2020年10月09日
796 阅读
0 评论
0 点赞
2020-09-29
并发性:互斥与同步
操作系统设计中的核心问题是关于进程和线程的管理。并发是所有问题的基础,也是操作系统设计的基础。并发包括许多设计问题,其中有进程间通信、资源共享与竞争(例如内存、文件、I/O访问)、多个进程活动的同步以及分配给进程的处理器时间等。和并发相关的一些关键术语:原子操作一个或多个指令的序列,对外是不可分的;即没有其他进程可以看到其中间状态或者中断此操作临界区是一段代码,在这段代码中进程将访问共享资源,当另外一个进程已经在这段代码中运行时,这个进程就不能在这段代码中执行。死锁两个或两个以上的进程因其中的每个进程都在等待其他进程做完某些事情而不能继续执行,这样的情形叫做死锁活锁两个或两个以上进程为了响应其他进程中的变化而持续改变自己的状态但不做有用的工作,这样的情形叫做活锁互斥当一个进程在临界区访问共享资源时,其他进程不能进入该临界区访问任何共享资源,这种情形叫做互斥竞争条件多个线程或者进程在读写一个共享数据时,结果依赖于它们执行的相对时间,这种情形叫做竞争饥饿是指一个可运行的进程尽管可能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况1.并发的原理在单处理器系统的情况下,出现问题的原因是中断可能会在进程中任何地方停止指令的执行;在多处理器系统的情况下,不仅同样的条件可以引发问题,而且当两个进程同时执行并且都试图访问同一个全局变量时,也会引发问题。这两类问题的解决方案是相同的:控制对共享资源的访问1.1 竞争条件竞争条件发生在多个进程或线程读写数据时,其最终的结果依赖于多个进程的指令执行顺序。1.2 操作系统关注的问题并发会带来哪些设计和管理问题?操作系统必须能够记住各个活跃的进程操作系统必须为每个进程分配和释放各种资源。操作系统必须保护每个进程的数据和物理资源,避免其他进程的无意干涉一个进程的功能和输出结果必须与执行速度无关。1.3 进程的交互知道程度关系一个进程对其他进程的影响潜在的控制问题进程之间不知道对方的存在竞争1.一个进程的结果与其他进程的活动无关 2.进程的执行时间可能会受影响互斥、死锁(可复用的资源)、饥饿进程间接知道对方的存在(如共享对象)通过共享合作1.一个进程的结果可能依赖于从其他进程获得的信息 2.进程的执行时间可能会受到影响互斥、死锁(可复用的资源)、饥饿、数据一致性进程直接知道对方的存在(它们有可用的通信原语)通过通信合作1.一个进程的结果可能依赖于从其他进程获得的信息2.进程的计时可能会受到影响死锁(可消费的资源)、饥饿2. 信号量现在讨论操作系统和用于提供并发性的程序设计语言机制。基本原理:两个或多个进程可以通过简单的信号进行合作,一个进程可以被迫在某一位置停止,直到它接收到一个特定的信号。任何复杂的合作需求都可以通过适当的信号结构得到满足,为了发信号,需要使用一个被称作信号量的特殊变量。为通过信号量s发送信号,进程可执行原语semSignal(s)(V操作);为通过信号量s接收信号,进程可执行原语semWait(s)(P操作);如果相应的信号仍然没有发送,则进程被挂起,直到发送完为止。为了达到预期的效果,可以把信号量看做是一个具有整数值的变量,在它之上定义三个操作:1)一个信号量可以初始化成非负数2)semWait操作使信号量减1。如果值变成负数,则执行semWait的进程被阻塞。否则进程继续执行。3)semSignal操作使信号量加1。如果值小于或者等于零,则被semWait操作阻塞的进程被解除阻塞。除了这三种操作外,没有任何其他方法可以检查或操作信号量。解释:开始时,信号量的值为零或正数。如果该值为正数,则该值等于发出semWait操作后可立即继续执行的进程的数量。如果该值为零(或者由于初始化,或者由于有等于信号量初值的进程已经等待),则发出semWait操作的下一个进程会被阻塞,此时该信号量的值变为负值。之后,每个后续的semWait操作都会使信号量的负值更大。该负值等于正在等待接触阻塞的进程的数量。在信号量为负值的情形下,每一个semSignal操作都会将等待进程中的一个进程解除阻塞。在信号量为负值的情形下,每一个semSignal操作都会将等待进程中的一个进程解除阻塞。2.1 互斥使用信号量s解决互斥问题的方法。设有n个进程,用数组P(i)表示,所有的进程都需要访问共享资源。每个进程中进入临界区前执行semWait(s),如果s的值为负,则进程被挂起;如果值为1,则s被减为0,进程立即进入临界区;由于s值不再为正,因而其他任何进程都不能进入临界区。/* program mutualexeclusion */ const int n = /* 进程数 */; semaphore s = 1; void P(int i) { while(true){ semWait(s); /* 临界区 */ semSignal(s); /* 其他部分 */ } } void main() { parbegin(P(1), P(2), ..., P(n)); }
2020年09月29日
839 阅读
0 评论
0 点赞
2020-09-24
c语言的左移和右移
https://www.cnblogs.com/lyggqm/p/7826938.html
2020年09月24日
857 阅读
0 评论
0 点赞
2020-09-24
Linux源码目录结构
https://blog.csdn.net/qq_38892883/article/details/79557715https://www.cnblogs.com/Senchuangdianzi/p/12120354.html
2020年09月24日
676 阅读
0 评论
0 点赞
2020-09-24
(1)进程内存管理初探
[TOC]随着cpu技术发展,现在大部分移动设备、PC、服务器都已经使用上64bit的CPU,但是关于Linux内核的虚拟内存管理,还停留在历史的用户态与内核态虚拟内存3:1的观念中,导致在解决一些内存问题时存在误解。例如现在主流的移动设备操作系统Android,经常遇到进程使用大量内存导致被lmk杀死,分配不到内存而触发OOM/ANR,或者分配内存慢导致卡顿,内核态使用哪个分配内存的函数更合理等问题,有些涉及物理内存分配,有些涉及虚拟内存分配,如果不熟悉虚拟内存管理的技术知识,可能走很多弯路。本章节结合代码介绍进程虚拟内存布局以及进程的虚拟内存分配释放流程,涉及的代码是android-8.1, 内核版本kernel-4.9,架构是arm64。1. 几种地址的概念1.1 物理地址每片物理内存存储实际地址,例如一个8GB的内存,0x00000000表示第一个byte的地址,而0xFFFFFFFF表示的是最后一个byte的地址;物理地址的值与实际的内存条上的地址一一对应,物理地址的大小与cpu访问物理内存的总线宽度有一定的关系。1.2 线性地址为了保证系统多任务运行的安全性和可靠性(防止一个任务篡改系统或者其他任务的内存),CPU增加段页式内存管理;段基地址+段内偏移构成的地址就是线性地址;如果开启的分页内存管理,线性地址还要通过MMU计算才能转换出物理地址。1.3 逻辑地址每个进程运行时CPU看到的地址就是逻辑地址,实际上也是线性地址中的段内偏移地址,逻辑地址与段基地址可以计算出线性地址。进程在访问虚拟地址空间的任意合法地址时,都要按照逻辑地址->线性地址->物理地址的顺序换算才能找到对应的物理地址;由于段式内存管理存在性能、访问效率的问题,以及Linux要兼容各种CPU,在Linux内核中所有的用户态进程使用的同一个段,且段基地址都是0,如此既可以兼容的传统的段式内存管理,又可以通过页式内存映射更灵活的管理内存。由于同一个段基地址都是0,对每个进程来说,逻辑地址和线性地址是一样的;同时每个进程的PGD是不一样的,从而保证每个进程之间隔离,不同进程同一个虚拟地址映射的物理地址就不一样了。Linux系统采用延迟分配物理内存的策略,用户态进程每次分配内存时分配的都是虚拟内存,表示一段地址空间已经分配出来供进程使用;当进程第一次访问虚拟地址时,才会发现虚拟地址没有对应的物理内存,系统默认会触发缺页异常,从内核物理内存管理系统中分配物理页,建立页表中把虚拟地址映射到物理地址。对于缺页异常处理流程,页表创建/建立/销毁等操作在以后文章中介绍。2. 进程虚拟内存空间分布理论上,64bit地址支持访问的地址空间是[0, 2(64-1)],而实际上现有的应用程序都不会用这么大的地址空间,并且arm64芯片现在也不支持访问这么大的地址空间,arm64架构芯片最大支持访问48bit的地址空间。例如在Android系统中,整个虚拟地址空间分成两部分,如下图所示:其中[0x0001000000000000,0xFFFF000000000000]之间的地址是不规范地址,不能使用;该段内存把整个虚拟地址空间划分为两段,低段内存为进程用户态地址空间,高段内存为内核地址空间。参考代码arch\arm64\include\asm\memory.h):#define VA_BITS (CONFIG_ARM64_VA_BITS) #define VA_START (UL(0xffffffffffffffff)) << VA_BITS) #define PAGE_OFFSET (UL(0xffffffffffffffff)) << (VA_BITS - 1 )) 如果内核打开CONFIG_COMPAT选项,说明用户态既支持64位进程,也支持32位进程;由于32bit的地址最多可以访问的虚拟地址空间最多只有4GB,所以32位进程的用户态进程地址空间与64位进程是有区别的。32位进程的用户态地址空间是[0x0, 0x00000000FFFF_FFFF]64位进程的用户态地址空间是[0x0, 0x0000FFFFFFFF_FFFF]从代码看出,32bit进程用户空间大小是4GB,64bit进程的虚拟内存大小与CONFIG_ARM64_VA_BITS的值相关;如果CONFIG_ARM64_VA_BITS是48bit则可以达到256TB,现在的移动设备显然用不到这么大的内存空间,所以大部分Android设备中CONFIG_ARM64_VA_BITS默认配置的是39,即64bit进程的最大虚拟地址空间大小是512GB。虽然32bit或者64bit的进程在用户态内存空间大小不一样,但是当它们陷入到内核态后,访问的内核空间地址是没有差异的,都是从VA_START开始,直到0xFFFFFFFFFFFFFFFF结束,也是512GB。每个进程的虚拟地址空间主要分为如下几个区域(如图):参考:https://cloud.tencent.com/developer/article/1647582http://www.360doc.com/content/13/0915/09/8363527_314549128.shtml
2020年09月24日
944 阅读
0 评论
0 点赞
2020-09-15
c语言之va_list详解
[TOC]转载:https://blog.csdn.net/zhyjunfov/article/details/12017697一、介绍VA函数(variable argument function),参数个数可变函数,又称可变参数函数。C/C++编程中,系统提供给编程人员的va函数很少。printf()/scanf()系列函数,用于输入输出时格式化字符串;exec()系列函数,用于在程序中执行外部文件(main(int argc, char* argv[]算不算呢,与其说main()也是一个可变参数函数,倒不如说它是exec()经过封装后的具备特殊功能和意义的函数,至少在原理这一级上有很多相似之处)。由于参数个数的不确定,使va函数具有很大的灵活性,易用性,对没有使用过可变参数函数的编程人员很有诱惑力;那么,该如何编写自己的va函数,va函数的运用时机、编译实现又是如何。作者借本文谈谈自己关于va函数的一些浅见。二、从printf函数开始从大家都很熟悉的格式化字符串函数开始介绍可变参数函数。原型:int printf(const char * format, ...);参数format表示如何来格式字符串的指令,…表示可选参数,调用时传递给"..."的参数可有可无,根据实际情况而定。系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。int vprintf / vscanf(const char * format, va_list ap); // 从标准输入/输出格式化字符串 int vfprintf / vfsacanf(FILE * stream, const char * format, va_list ap); // 从文件流 int vsprintf / vsscanf(char * s, const char * format, va_list ap); // 从字符串例1:格式化到一个文件流,可用于日志文件FILE *logfile; int WriteLog(const char * format, ...) { va_list arg_ptr; va_start(arg_ptr, format); int nWrittenBytes = vfprintf(logfile, format, arg_ptr); va_end(arg_ptr); return nWrittenBytes; } …调用时,与使用printf()没有区别。WriteLog("%04d-%02d-%02d %02d:%02d:%02d %s/%04d logged out.", nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID);同理,也可以从文件中执行格式化输入;或者对标准输入输出,字符串执行格式化。在上面的例1中,WriteLog()函数可以接受参数个数可变的输入,本质上,它的实现需要vprintf()的支持。如何真正实现属于自己的可变参数函数,包括控制每一个传入的可选参数。三、va函数的定义和va宏C语言支持va函数,作为C语言的扩展--C++同样支持va函数,但在C++中并不推荐使用,C++引入的多态性同样可以实现参数个数可变的函数。不过,C++的重载功能毕竟只能是有限多个可以预见的参数个数。比较而言,C中的va函数则可以定义无穷多个相当于C++的重载函数,这方面C++是无能为力的。va函数的优势表现在使用的方便性和易用性上,可以使代码更简洁。C编译器为了统一在不同的硬件架构、硬件平台上的实现,和增加代码的可移植性,提供了一系列宏来屏蔽硬件环境不同带来的差异。ANSI C标准下,va的宏定义在stdarg.h中,它们有:va_list,va_start(),va_arg(),va_end()。例2:求任意个自然数的平方和:int SqSum(int n1, ...) { va_list arg_ptr; int nSqSum = 0, n = n1; va_start(arg_ptr, n1); while (n > 0) { nSqSum += (n * n); n = va_arg(arg_ptr, int); } va_end(arg_ptr); return nSqSum; } // 调用时 int nSqSum = SqSum(7, 2, 7, 11, -1);可变参数函数的原型声明格式为:type VAFunction(type arg1, type arg2, … );参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明时用"…"表示。固定参数和可选参数公同构成一个函数的参数列表。借助上面这个简单的例2,来看看各个va_xxx的作用。 va_list arg_ptr:定义一个指向个数可变的参数列表指针;va_start(arg_ptr, argN):使参数列表指针arg_ptr指向函数参数列表中的第一个可选参数说明:argN是位于第一个可选参数之前的固定参数,(或者说,最后一个固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。如果有一va函数的声明是void va_test(char a, char b, char c, …),则它的固定参数依次是a,b,c,最后一个固定参数argN为c,因此就是va_start(arg_ptr, c)。va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数。va_copy(dest, src):dest,src的类型都是va_list,va_copy()用于复制参数列表指针,将dest初始化为src。va_end(arg_ptr):清空参数列表,并置参数指针arg_ptr无效。说明:指针arg_ptr被置无效后,可以通过调用va_start()、va_copy()恢复arg_ptr。每次调用va_start() / va_copy()后,必须得有相应的va_end()与之匹配。参数指针可以在参数列表中随意地来回移动,但必须在va_start() … va_end()之内。四、编译器如何实现va例2中调用SqSum(7, 2, 7, 11, -1)来求7, 2, 7, 11的平方和,-1是结束标志。简单地说,va函数的实现就是对参数指针的使用和控制。typedef char * va_list; // x86平台下va_list的定义函数的固定参数部分,可以直接从函数定义时的参数名获得;对于可选参数部分,先将指针指向第一个可选参数,然后依次后移指针,根据与结束标志的比较来判断是否已经获得全部参数。因此,va函数中结束标志必须事先约定好,否则,指针会指向无效的内存地址,导致出错。这里,移动指针使其指向下一个参数,那么移动指针时的偏移量是多少呢,没有具体答案,因为这里涉及到内存对齐(alignment)问题,内存对齐跟具体使用的硬件平台有密切关系,比如大家熟知的32位x86平台规定所有的变量地址必须是4的倍数(sizeof(int) = 4)。va机制中用宏_INTSIZEOF(n)来解决这个问题,没有这些宏,va的可移植性无从谈起。首先介绍宏_INTSIZEOF(n),它求出变量占用内存空间的大小,是va的实现的基础。#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //第一个可选参数地址 #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址 #define va_end(ap) ( ap = (va_list)0 ) // 将指针置为无效下表是针对函数int TestFunc(int n1, int n2, int n3, …) 参数传递时的内存堆栈情况。(C编译器默认的参数传递方式是__cdecl。)对该函数的调用为int result = TestFunc(a, b, c, d. e); 其中e为结束标志。va_xxx宏如此编写的原因。1. va_start。为了得到第一个可选参数的地址,我们有三种办法可以做到:A) = &n3 + _INTSIZEOF(n3) // 最后一个固定参数的地址 + 该参数占用内存的大小B) = &n2 + _INTSIZEOF(n3) + _INTSIZEOF(n2) // 中间某个固定参数的地址 + 该参数之后所有固定参数占用的内存大小之和C) = &n1 + _INTSIZEOF(n3) + _INTSIZEOF(n2) + _INTSIZEOF(n1) // 第一个固定参数的地址 + 所有固定参数占用的内存大小之和从编译器实现角度来看,方法B),方法C)为了求出地址,编译器还需知道有多少个固定参数,以及它们的大小,没有把问题分解到最简单,所以不是很聪明的途径,不予采纳;相对来说,方法A)中运算的两个值则完全可以确定。va_start()正是采用A)方法,接受最后一个固定参数。调用va_start()的结果总是使指针指向下一个参数的地址,并把它作为第一个可选参数。在含多个固定参数的函数中,调用va_start()时,如果不是用最后一个固定参数,对于编译器来说,可选参数的个数已经增加,将给程序带来一些意想不到的错误。(当然如果你认为自己对指针已经知根知底,游刃有余,那么,怎么用就随你,你甚至可以用它完成一些很优秀(高效)的代码,但是,这样会大大降低代码的可读性。)注意:宏va_start是对参数的地址进行操作的,要求参数地址必须是有效的。一些地址无效的类型不能当作固定参数类型。比如:寄存器类型,它的地址不是有效的内存地址值;数组和函数也不允许,他们的长度是个问题。因此,这些类型时不能作为va函数的参数的。2. va_arg身兼二职:返回当前参数,并使参数指针指向下一个参数。初看va_arg宏定义很别扭,如果把它拆成两个语句,可以很清楚地看出它完成的两个职责。#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //下一个参数地址 // 将( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )拆成: /* 指针ap指向下一个参数的地址 */ 1. ap += _INTSIZEOF(t); // 当前,ap已经指向下一个参数了 /* ap减去当前参数的大小得到当前参数的地址,再强制类型转换后返回它的值 */ 2. return *(t *)( ap - _INTSIZEOF(t))回想到printf/scanf系列函数的%d %s之类的格式化指令,我们不难理解这些它们的用途了- 明示参数强制转换的类型。(注:printf/scanf没有使用va_xxx来实现,但原理是一致的。)3.va_end很简单,仅仅是把指针作废而已。#define va_end(ap) (ap = (va_list)0) // x86平台五、简洁、灵活,也有风险从va的实现可以看出,指针的合理运用,把C语言简洁、灵活的特性表现得淋漓尽致,叫人不得不佩服C的强大和高效。不可否认的是,给编程人员太多自由空间必然使程序的安全性降低。va中,为了得到所有传递给函数的参数,需要用va_arg依次遍历。其中存在两个隐患:1)如何确定参数的类型。 va_arg在类型检查方面与其说非常灵活,不如说是很不负责,因为是强制类型转换,va_arg都把当前指针所指向的内容强制转换到指定类型;2)结束标志。如果没有结束标志的判断,va将按默认类型依次返回内存中的内容,直到访问到非法内存而出错退出。例2中SqSum()求的是自然数的平方和,所以我把负数和0作为它的结束标志。例如scanf把接收到的回车符作为结束标志,大家熟知的printf()对字符串的处理用'\0'作为结束标志,无法想象C中的字符串如果没有'\0', 代码将会是怎样一番情景,估计那时最流行的可能是字符数组,或者是malloc/free。允许对内存的随意访问,会留给不怀好意者留下攻击的可能。当处理cracker精心设计好的一串字符串后,程序将跳转到一些恶意代码区域执行,以使cracker达到其攻击目的。(常见的exploit攻击)所以,必需禁止对内存的随意访问和严格控制内存访问边界。六、Unix System V兼容方式的va声明上面介绍可变参数函数的声明是采用ANSI标准的,Unix System V兼容方式的声明有一点点区别,它增加了两个宏:va_alist,va_dcl。而且它们不是定义在stdarg.h中,而是varargs.h中。stdarg.h是ANSI标准的;varargs.h仅仅是为了能与以前的程序保持兼容而出现的,现在的编程中不推荐使用。va_alist:函数声明/定义时出现在函数头,用以接受参数列表。va_dcl:对va_alist的声明,其后无需跟分号";"va_start的定义也不相同。因为System V可变参数函数声明不区分固定参数和可选参数,直接对参数列表操作。所以va_start()不是va_start(ap,v),而是简化为va_start(ap)。其中,ap是va_list型的参数指针。Unix System V兼容方式下函数的声明形式:type VAFunction(va_alist) va_dcl // 这里无需分号 { // 函数体内同ANSI标准 }例3:猜测execl的实现(Unix System V兼容方式),摘自SUS V2#include <varargs.h> #define MAXARGS 100 / * execl(file, arg1, arg2, ..., (char *)0); */ execl(va_alist) va_dcl { va_list ap; char *file; char *args[MAXARGS]; int argno = 0; va_start(ap); file = va_arg(ap, char *); while ((args[argno++] = va_arg(ap, char *)) != (char *)0) ; va_end(ap); return execv(file, args); }七、扩展与思考个数可变参数在声明时只需"..."即可;但是,我们在接受这些参数时不能"..."。va函数实现的关键就是如何得到参数列表中可选参数,包括参数的值和类型。以上的所有实现都是基于来自stdarg.h的va_xxx的宏定义。 <思考>能不能不借助于va_xxx,自己实现VA呢?,我想到的方法是汇编。在C中,我们当然就用C的嵌入汇编来实现,这应该是可以做得到的。至于能做到什么程度,稳定性和效率怎么样,主要要看你对内存和指针的控制了。八、写一个简单的可变参数的C函数下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的 C函数要在程序中用到以下这些宏:void va_start( va_list arg_ptr, prev_param ); type va_arg( va_list arg_ptr, type ); void va_end( va_list arg_ptr ); va在这里是variable-argument(可变参数)的意思. 这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个 头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数 参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.void simple_va_fun(int i, ...) { va_list arg_ptr; int j=0; va_start(arg_ptr, i); j=va_arg(arg_ptr, int); va_end(arg_ptr); printf("%d %d\n", i, j); return; } 我们可以在我们的头文件中这样声明我们的函数: extern void simple_va_fun(int i, ...); 我们在程序中可以这样调用: simple_va_fun(100); simple_va_fun(100,200); 从这个函数的实现可以看到,我们使用可变参数应该有以下步骤: 1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变 量是指向参数的指针. 2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第 一个可变参数的前一个参数,是一个固定的参数. 3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个 参数是你要返回的参数的类型,这里是int型. 4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使 用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获 取各个参数. 如果我们用下面三种方法调用的话,都是合法的,但结果却不一样: 1)simple_va_fun(100); 结果是:100 -123456789(会变的值) 2)simple_va_fun(100,200); 结果是:100 200 3)simple_va_fun(100,200,300); 结果是:100 200 我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果 正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果 的原因和可变参数在编译器中是如何处理的. (二)可变参数在编译器中的处理 我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下 面以VC++中stdarg.h里x86平台的宏定义摘录如下(’\’号表示折行):typedef char * va_list; #define _INTSIZEOF(n) \ ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) #define va_arg(ap,t) \ ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ( ap = (va_list)0 ) 定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函 数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我 们看到va_list被定义成char,有一些平台或操作系统定义为void.再 看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的 地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆 栈的地址,如图: 高地址|-----------------------------| |函数返回地址 | |-----------------------------| |....... | |-----------------------------| |第n个参数(第一个可变参数) | |-----------------------------|<--va_start后ap指向 |第n-1个参数(最后一个固定参数)| 低地址|-----------------------------|<-- &v 图( 1 ) 然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我 们看一下va_arg取int型的返回值: j= ( *(int*)((ap += \_INTSIZEOF(int))-\_INTSIZEOF(int)) ); 首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回 ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址 (图2).然后用*取得这个地址的内容(参数值)赋给j. 高地址|-----------------------------| |函数返回地址 | |-----------------------------| |....... | |-----------------------------|<--va_arg后ap指向 |第n个参数(第一个可变参数) | |-----------------------------|<--va_start后ap指向 |第n-1个参数(最后一个固定参数)| 低地址|-----------------------------|<-- &v 图( 2 )最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不 会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所 以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的. (三)可变参数在编程中要注意的问题 因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢, 可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能 地识别不同参数的个数和类型. 有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数 printf是从固定参数format字符串来分析出参数的类型,再调用va_arg 的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通 过在自己的程序里作判断来实现的. 另外有一个问题,因为编译器对可变参数的函数的原型检查不够严 格,对编程查错不利.如果simple_va_fun()改为:void simple_va_fun(int i, ...) { va_list arg_ptr; char *s=NULL; va_start(arg_ptr, i); s=va_arg(arg_ptr, char*); va_end(arg_ptr); printf("%d %s\n", i, s); return; } 可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现 core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出 错,但错误却是难以发现,不利于我们写出高质量的程序. 以下提一下va系列宏的兼容性. System V Unix把va_start定义为只有一个参数的宏: va_start(va_list arg_ptr); 而ANSI C则定义为: va_start(va_list arg_ptr, prev_param); 如果我们要用system V的定义,应该用vararg.h头文件中所定义的 宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以 用ANSI C的定义就够了,也便于程序的移植.
2020年09月15日
1,828 阅读
0 评论
0 点赞
2020-09-15
c语言的__attribute__
[TOC]转载:https://blog.csdn.net/qlexcel/article/details/92656797https://www.cnblogs.com/embedded-linux/p/5801999.html一、介绍GNU C 的一大特色就是\_\_attribute\_\_ 机制。\_\_attribute\_\_ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )。\_\_attribute\_\_ 书写特征是:\_\_attribute\_\_ 前后都有两个下划线,并切后面会紧跟一对原括弧,\_\_attribute\_\_ 参数。\_\_attribute\_\_ 语法格式为:\_\_attribute\_\_ ((attribute-list))\_\_attribute\_\_ 也可以对结构体(struct )或共用体(union )进行属性设置。大致有六个参数值可以被设定,即:aligned, packed, transparent_union, unused, deprecated 和 may_alias 。\_\_attribute\_\_ 参数时,你也可以在参数的前后都加上“\_\_” (两个下划线),例如,使用\_\_aligned\_\_而不是aligned ,这样,你就可以在相应的头文件里使用它而不用关心头文件里是否有重名的宏定义。二、__attribute__参数介绍1.aligned该属性设定一个指定大小的对齐格式(以字节 为单位),例如:struct S { short b[3]; } __attribute__ ((aligned (8))); typedef int int32_t __attribute__ ((aligned (8)));该声明将强制编译器确保(尽它所能)变量类 型为struct S 或者int32_t 的变量在分配空间时采用8 字节对齐方式。如上所述,你可以手动指定对齐的格式,同 样,你也可以使用默认的对齐方式。如果aligned 后面不紧跟一个指定的数字值,那么编译器将依据你的目标机器情况使用最大最有益的对齐方式。例如:struct S { short b[3]; } __attribute__ ((aligned));这里,如果sizeof (short )的大小为2 (byte ),那么,S 的大小就为6 。取一个2 的次方值,使得该值大于等于6 ,则该值为8 ,所以编译器将设置S 类型的对齐方式为8 字节。ligned 属性使被设置的对象占用更多的空间,相反的,使用packed 可以减小对象占用的空间。需要注意的是,attribute 属性的效力与你的连接器也有关,如果你的连接器最大只支持16 字节对齐,那么你此时定义32 字节对齐也是无济于事的。2.packed使用该属性对struct 或者union 类型进行定义,设定其类型的每一个变量的内存约束。当用在enum 类型 定义时,暗示了应该使用最小完整的类型(it indicates that the smallest integral type should be used)。下面的例子中,packed_struct 类型的变量数组中的值将会紧紧的靠在一起,但内部的成员变量s 不会被“pack” ,如果希望内部的成员变量也被packed 的话,unpacked-struct 也需要使用packed 进行相应的约束。struct unpacked_struct { char c; int i; }; struct packed_struct { char c; int i; struct unpacked_struct s; }__attribute__ ((__packed__));下面的例子中使用\_\_attribute\_\_ 属性定义了一些结构体及其变量,并给出了输出结果和对结果的分析。struct p { int a; char b; short c; }__attribute__((aligned(4))) pp; struct m { char a; int b; short c; }__attribute__((aligned(4))) mm; struct o { int a; char b; short c; }oo; struct x { int a; char b; struct p px; short c; }__attribute__((aligned(8))) xx; int main() { printf("sizeof(int)=%d,sizeof(short)=%d.sizeof(char)=%d\n",sizeof(int),sizeof(short),sizeof(char)); printf("pp=%d,mm=%d \n", sizeof(pp),sizeof(mm)); printf("oo=%d,xx=%d \n", sizeof(oo),sizeof(xx)); return 0; }输出结 果:sizeof(int)=4,sizeof(short)=2.sizeof(char)=1pp=8,mm=12oo=8,xx=24分析:sizeof(pp):sizeof(a)+sizeof(b)+sizeof(c)=4+1+1=6\<8 所以sizeof(pp)=8sizeof(mm):sizeof(a)+sizeof(b)+sizeof(c)=1+4+2=7但是 a 后面需要用 3 个字节填充,但是 b 是 4 个字节,所以 a 占用 4 字节, b 占用 4 个字节,而 c 又要占用 4 个字节。所以 sizeof(mm)=12sizeof(oo):sizeof(a)+sizeof(b)+sizeof(c)=4+1+2=7因为默 认是以4 字节对齐,所以sizeof(oo)=8sizeof(xx):sizeof(a)+ sizeof(b)=4+1=5sizeof(pp)=8; 即xx 是采用8 字节对齐的,所以要在a ,b 后面添3 个空余字节,然后才能存储px ,4+1+ (3 )+8+1=17因为xx 采用的对齐是8 字节对齐,所以xx 的大小必定是8 的整数倍,即xx 的大小是一个比17 大又是8 的倍数的一个最小值,由此得到17\<24 ,所以sizeof(xx)=243.函数属性(Function Attribute)函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。\_\_attribute\_\_机制也很容易同非GNU应用程序做到兼容之功效。GNU CC需要使用 –Wall编译器来击活该功能,这是控制警告信息的一个很好的方式。下面介绍几个常见的属性参数。\_\_attribute\_\_ format该\_\_attribute\_\_属性可以给被声明的函数加上类似printf或者scanf的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。该功能十分有用,尤其是处理一些很难发现的bug。format的语法格式为:format (archetype, string-index, first-to-check)format属性告诉编译器,按照printf, scanf, strftime或strfmon的参数表格式规则对该函数的参数进行检查。“archetype”指定是哪种风格;“string-index”指定传入函数的第几个参数是格式化字符串;“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。具体使用格式如下:\_\_attribute\_\_((format(printf,m,n)))\_\_attribute\_\_((format(scanf,m,n)))其中参数m与n的含义为:m:第几个参数为格式化字符串(format string);n:参数集合中的第一个,即参数“…”里的第一个参数在函数参数总数排在第几,注意,有时函数参数里还有“隐身”的呢,后面会提到;在使用上,\_\_attribute\_\_((format(printf,m,n)))是常用的,而另一种却很少见到。下面举例说明,其中myprint为自己定义的一个带有可变参数的函数,其功能类似于printf://m=1;n=2 extern void myprint(const char *format,...) \_\_attribute\_\_((format(printf,1,2))); //m=2;n=3 extern void myprint(int l,const char *format,...) \_\_attribute\_\_((format(printf,2,3)));需要特别注意的是,如果myprint是一个函数的成员函数,那么m和n的值可有点“悬乎”了,例如://m=3;n=4 extern void myprint(int l,const char *format,...) \_\_attribute\_\_((format(printf,3,4)));其原因是,类成员函数的第一个参数实际上一个“隐身”的“this”指针。(有点C++基础的都知道点this指针,不知道你在这里还知道吗?)这里给出测试用例:attribute.c,代码如下:1: 2:extern void myprint(const char *format,...) __attribute__((format(printf,1,2))); 3: 4:void test() 5:{ 6: myprint("i=%d\n",6); 7: myprint("i=%s\n",6); 8: myprint("i=%s\n","abc"); 9: myprint("%s,%d,%d\n",1,2); 10:}运行$gcc –Wall –c attribute.c attribute后,输出结果为:attribute.c: In function 'test':attribute.c: 7: warning: format argument is not a pointer (arg 2)attribute.c: 9: warning: format argument is not a pointer (arg 2)attribute.c: 9: warning: too few arguments for format如果在attribute.c中的函数声明去掉\_\_attribute\_\_((format(printf,1,2))),再重新编译,既运行$gcc –Wall –c attribute.c attribute后,则并不会输出任何警告信息。注意,默认情况下,编译器是能识别类似printf的“标准”库函数。\_\_attribute\_\_ noreturn该属性通知编译器函数从不返回值,当遇到类似函数需要返回值而却不可能运行到返回值处就已经退出来的情况,该属性可以避免出现错误信息。C库函数中的abort()和exit()的声明格式就采用了这种格式,如下所示:extern void exit(int) \_\_attribute\_\_((noreturn));extern void abort(void) \_\_attribute\_\_((noreturn)); 为了方便理解,大家可以参考如下的例子://name: noreturn.c ;测试__attribute__((noreturn)) extern void myexit(); int test(int n) { if ( n > 0 ) { myexit(); /* 程序不可能到达这里*/ } else return 0; }编译显示的输出信息为:gcc –Wall –c noreturn.cnoreturn.c: In function 'test':noreturn.c: 12: warning: control reaches end of non-void function警告信息也很好理解,因为你定义了一个有返回值的函数test却有可能没有返回值,程序当然不知道怎么办了!加上\_\_attribute\_\_((noreturn))则可以很好的处理类似这种问题。把extern void myexit();修改为:extern void myexit() \_\_attribute\_\_((noreturn));之后,编译不会再出现警告信息。\_\_attribute\_\_ const该属性只能用于带有数值类型参数的函数上。当重复调用带有数值参数的函数时,由于返回值是相同的,所以此时编译器可以进行优化处理,除第一次需要运算外, 其它只需要返回第一次的结果就可以了,进而可以提高效率。该属性主要适用于没有静态状态(static state)和副作用的一些函数,并且返回值仅仅依赖输入的参数。为了说明问题,下面举个非常“糟糕”的例子,该例子将重复调用一个带有相同参数值的函数,具体如下:extern int square(int n) __attribute__ ((const)); ... for (i = 0; i < 100; i++ ) { total += square (5) + i; }通过添加\_\_attribute\_\_((const))声明,编译器只调用了函数一次,以后只是直接得到了相同的一个返回值。事实上,const参数不能用在带有指针类型参数的函数中,因为该属性不但影响函数的参数值,同样也影响到了参数指向的数据,它可能会对代码本身产生严重甚至是不可恢复的严重后果。并且,带有该属性的函数不能有任何副作用或者是静态的状态,所以,类似getchar()或time()的函数是不适合使用该属性的。关于linux内核中的"\_\_attribute\_\_ ((packed))"引用:\_\_attribute\_\_ ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。#define __u8 unsigned char #define __u16 unsigned short /* __attribute__ ((packed)) 的位置约束是放于声明的尾部“;”之前 */ struct str_struct{ __u8 a; __u8 b; __u8 c; __u16 d; } __attribute__ ((packed)); /* 当用到typedef时,要特别注意__attribute__ ((packed))放置的位置,相当于: * typedef struct str_stuct str; * 而struct str_struct 就是上面的那个结构。 */ typedef struct { __u8 a; __u8 b; __u8 c; __u16 d; } __attribute__ ((packed)) str; /* 在下面这个typedef结构中,__attribute__ ((packed))放在结构名str_temp之后,其作用是被忽略的,注意与结构str的区别。*/ typedef struct { __u8 a; __u8 b; __u8 c; __u16 d; }str_temp __attribute__ ((packed)); typedef struct { __u8 a; __u8 b; __u8 c; __u16 d; }str_nopacked; int main(void) { printf("sizeof str = %d\n", sizeof(str)); printf("sizeof str_struct = %d\n", sizeof(struct str_struct)); printf("sizeof str_temp = %d\n", sizeof(str_temp)); printf("sizeof str_nopacked = %d\n", sizeof(str_nopacked)); return 0; }编译运行:引用:[root@localhost root]# ./packedtest sizeof str = 5sizeof str_struct = 5sizeof str_temp = 6sizeof str_nopacked = 6packed属性:使用该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐。4.at绝对定位,可以把变量或函数绝对定位到Flash中,或者定位到RAM。1)、定位到flash中,一般用于固化的信息,如出厂设置的参数,上位机配置的参数,ID卡的ID号,flash标记等等const u16 gFlashDefValue[512] __attribute__((at(0x0800F000))) = {0x1111,0x1111,0x1111,0x0111,0x0111,0x0111};//定位在flash中,其他flash补充为00 const u16 gflashdata__attribute__((at(0x0800F000))) = 0xFFFF;2)、定位到RAM中,一般用于数据量比较大的缓存,如串口的接收缓存,再就是某个位置的特定变量u8 USART2_RX_BUF[USART2_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.注意:1)、绝对定位不能在函数中定义,局部变量是定义在栈区的,栈区由MDK自动分配、释放,不能定义为绝对地址,只能放在函数外定义。2)、定义的长度不能超过栈或Flash的大小,否则,造成栈、Flash溢出。5.section提到section,就得说RO RI ZI了,在ARM编译器编译之后,代码被划分为不同的段,RO Section(ReadOnly)中存放代码段和常量,RW Section(ReadWrite)中存放可读写静态变量和全局变量,ZI Section(ZeroInit)是存放在RW段中初始化为0的变量。于是本文的大体意思就清晰了,\_\_attribute\_\_((section("section_name"))),其作用是将作用的函数或数据放入指定名为"section_name"对应的段中。1)、编译时为变量指定段:__attribute__((section("name"))) RealView Compilation Tools for µVision Compiler Reference Guide Version 4.0 Home > Compiler-specific Features > Variable attributes > __attribute__((section("name"))) 4.5.6. __attribute__((section("name"))) Normally, the ARM compiler places the objects it generates in sections like data and bss. However, you might require additional data sections or you might want a variable to appear in a special section, for example, to map to special hardware. The section attribute specifies that a variable must be placed in a particular data section. If you use the section attribute, read-only variables are placed in RO data sections, read-write variables are placed in RW data sections unless you use the zero_init attribute. In this case, the variable is placed in a ZI section. Note This variable attribute is a GNU compiler extension supported by the ARM compiler. Example /* in RO section */ const int descriptor[3] __attribute__ ((section ("descr"))) = { 1,2,3 }; /* in RW section */ long long rw[10] __attribute__ ((section ("RW"))); /* in ZI section * long long altstack[10] __attribute__ ((section ("STACK"), zero_init));/2)、编译时为函数指定段__attribute__((section("name"))) RealView Compilation Tools for µVision Compiler Reference Guide Version 4.0 Home > Compiler-specific Features > Function attributes > __attribute__((section("name"))) 4.3.13. __attribute__((section("name"))) The section function attribute enables you to place code in different sections of the image. Note This function attribute is a GNU compiler extension that is supported by the ARM compiler. Example In the following example, Function_Attributes_section_0 is placed into the RO section new_section rather than .text. void Function_Attributes_section_0 (void) __attribute__ ((section ("new_section"))); void Function_Attributes_section_0 (void) { static int aStatic =0; aStatic++; } In the following example, section function attribute overrides the #pragma arm section setting. #pragma arm section code="foo" int f2() { return 1; } // into the 'foo' area __attribute__ ((section ("bar"))) int f3() { return 1; } // into the 'bar' area int f4() { return 1; } // into the 'foo' area #pragma arm section6.多个属性组合使用u8 FileAddr[100] __attribute__ ((section ("FILE_RAM"), zero_init,aligned(4)));
2020年09月15日
3,184 阅读
0 评论
0 点赞
2020-09-15
内核日志:API及实现
[TOC]内核版本:Linux5.4.0本文首先通过介绍用于配置和收集日志信息的应用程序接口(API)来说明了内核的日志(见图 1 关于总结框架和组件的示意图)。然后,本文介绍了日志数据从内核到用户空间的移动过程。最后,本文还介绍了基于内核的日志数据的目标:用户空间中使用 rsyslog 进行日志管理。内核API内核的日志是通过 printk 函数实现的,它与用户空间对应函数 printf(按格式打印)具有相似的作用。printf 命令在编程语言中已存在很长时间,最近出现是在 C 语言中,但是最早出现可以追溯到 50 年代和 60 年代的 Fortran(PRINT 和 FORMAT 语句)、BCPL(writf 函数;BCPL 是 C 的前身)和 ALGOL 68 语言(printf 和 putf)。在内核中,printk(打印内核)可以使用与 printf 函数几乎一样的格式将将格式化消息写入到缓冲区。您可以在 ./linux/include/linux/kernel.h(及其实现 ./linux/kernel/printk.c)中看到 printk 的格式:int printk( const char * fmt, ... );这个格式表示的是一个用于定义文本和格式的字符串(类似于 printf),它同时带有一组可变个数参数(由省略号表示 [...])。内核配置与错误通过 printk 实现的日志是通过内核配置选项 CONFIG_PRINTK 激活的。虽然 CONFIG_PRINTK 一般都是激活的,但是不包含这个选项的系统对内核的调用会返回一个 ENOSYS 错误返回值。在使用 printk 时,您首先会发现的不同点更多是关于协议,而不是功能的。这个特性使用了 C 语言的一种模糊方面来简化消息级别和优先级的规范。内核允许每一个消息根据日志级别(定义不同消息重要必的八种级别之一)来分类。这些级别可以用来判断系统是否不可用(紧急消息)、是否发现严重状况(严重消息)或者是否为简单报告消息。 这个内核代码直接将日志级别定义消息的第一个参数,下面这个例子说明的就是严重消息的定义:printk( KERN_CRIT "Error code %08x.\n", val );注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(KERN_CRIT)和格式字符的逗号(,)。KERN_CRIT 本身只是一个普通的字符串(事实上,它表示的是字符串 "2";表 1 列出了完整的日志级别清单)。作为预处理程序的一部分,C 会自动地使用一个名为 字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。注意,如果调用者未将日志级别提供给 printk,那么系统就会使用默认值 KERN_WARNING(表示只有 KERN_WARNING 级别以上的日志消息会被记录。)在头文件include/linux/kernel_levels.h里面,定义了日志级别和使用方法:// 位置:include/linux/kernel_levels.h #define KERN_SOH "\001" /* ASCII Start Of Header */ #define KERN_SOH_ASCII '\001' #define KERN_EMERG KERN_SOH "0" /* system is unusable */ #define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */ #define KERN_CRIT KERN_SOH "2" /* critical conditions */ #define KERN_ERR KERN_SOH "3" /* error conditions */ #define KERN_WARNING KERN_SOH "4" /* warning conditions */ #define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */ #define KERN_INFO KERN_SOH "6" /* informational */ #define KERN_DEBUG KERN_SOH "7" /* debug-level messages */ #define KERN_DEFAULT KERN_SOH "d" /* the default kernel loglevel */printk可以在内核的任意上下文中调用。这个调用从 ./linux/kernel/printk.c 中的 printk 函数开始,它会在使用 va_start 解析可变长度参数之后调用 vprintk(在同一个源文件)。printk函数的实现如下:asmlinkage __visible int printk(const char *fmt, ...) { va_list args; // 可变参数 int r; va_start(args, fmt); r = vprintk_func(fmt, args); va_end(args); return r; } EXPORT_SYMBOL(printk);其中vprintk_func()函数定以如下:__printf(1, 0) int vprintk_func(const char *fmt, va_list args) { /* * Try to use the main logbuf even in NMI. But avoid calling console * drivers that might have their own locks. */ if ((this_cpu_read(printk_context) & PRINTK_NMI_DIRECT_CONTEXT_MASK) && raw_spin_trylock(&logbuf_lock)) { int len; len = vprintk_store(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args); raw_spin_unlock(&logbuf_lock); defer_console_output(); return len; } /* Use extra buffer in NMI when logbuf_lock is taken or in safe mode. */ if (this_cpu_read(printk_context) & PRINTK_NMI_CONTEXT_MASK) return vprintk_nmi(fmt, args); /* Use extra buffer to prevent a recursion deadlock in safe mode. */ if (this_cpu_read(printk_context) & PRINTK_SAFE_CONTEXT_MASK) return vprintk_safe(fmt, args); /* No obstacles. */ return vprintk_default(fmt, args); }vprintk 函数执行了许多管理级检查(递归检查),然后获取日志缓冲区的锁(\_\_log_buf)。接下来,它会对输入的字符串进行日志级别检查; 如果发现日志级别信息,那么对应的日志级别就会被设置。最后,vprintk 会获取当前时间(使用函数 cpu_clock)并使用 sprintf(不是标准库版本,而是在 ./linux/lib/vsprintf.c 中实现的内部内核版本)将它转换成一个字符串。这个字符串会被传递给 printk,然后它会被一个管理缓冲边界(emit_log_char)的特殊函数复制到内核日志缓冲区中。这个函数最后将获取和释放执行控制台信号,并将下一条日志消息发送到控制台(在 release_console_sem 中执行)。内核缓冲缓冲区的大小初始值为 4KB,但是最新的内核大小已经升级到 16KB(在不同的体系架构上,这个值最高可以达到 1MB)。日志辅助函数内核也提供了一些日志辅助函数,它们可以简化日志函数的使用。每一个日志级别都有一个对应的函数,它会扩展为 printk 函数的一个宏。例如,如果要使用 printk 处理 KERN_EMERG 日志级别时,您可以直接使用 pr_emerg。所有宏都已列在 ./linux/include/linux/kernel.h 文件中。至此,您已经了解用于将日志消息插入到内核环缓冲区的 API。现在,让我们讨论一下用于将数据从内核移动到用户空间的方法。内核日志与接口多用途的 syslog 系统调用提供了内核的日志缓冲区访问方法。这个调用执行了很多个操作,所有操作都可以在用户空间执行,但是只有一个操作可以被非 root 用户执行。syslog 系统调用的原型定义位于 ./linux/include/linux/syslog.h;而它的实现位于 ./linux/kernel/printk.c。syslog 调用是作为内核日志消息环缓冲区的输入/输出(I/O)和控制接口。通过 syslog 调用,应用程序可以读取日志消息(部分、整体或者只读取新消息), 以及控制环缓冲区的行为(清除内容、设置日志的消息级别、启用或禁用控制台等等)。图 2 用图形说明了使用所讨论的主要组件进行日志记录的过程。syslog 调用(在内核中调用 ./linux/kernel/printk.c 的 do_syslog)是一个相对较小的函数,它能够读取和控制内核环缓冲区。注意在 glibc 2.0 中,由于词汇 syslog 使用过于广泛,这个函数的名称被修改成 klogctl,它指的是各种调用和应用程序。syslog 和 klogctl(在用户空间中)的原型函数定义为:int syslog( int type, char *bufp, int len ); int klogctl( int type, char *bufp, int len );type 参数是用于传递所执行的命令,它指定了可选的缓冲区长度。有一些命令(如清除环缓冲)是忽略 bufp 和 len 这两个参数的。虽然前面两个命令类型不会对内核进行任何操作,但是其余命令则是用于读取日志消息或控制日志。其中有三个命令是用于读取日志消息的。SYSLOG_ACTION_READ 用于阻塞操作,直至日志消息到达后才释放该操作,然后将它们返回到所提供的缓冲区。这个命令会处理这些消息(旧的消息将不会出现在这个命令的后续调用中。)SYSLOG_ACTION_READ_ALL 命令会从日志读取最后 n 个字符(而 n 是在传递给 klogctl 的参数 'len' 中定义的)。SYSLOG_ACTION_READ_CLEAR 命令会先执行 SYSLOG_ACTION_READ_ALL 操作,然后执行 SYSLOG_ACTION_CLEAR 命令(清除环缓冲区)。SYSLOG_ACTION_CONSOLE ON 和 OFF 可以将日志级别设置为激活或禁用日志消息输出到控制台,而 SYSLOG_CONSOLE_LEVEL 则允许调用者定义控制台所接受的日志消息级别。最后,SYSLOG_ACTION_SIZE_BUFFER 是用于返回内核环缓冲区大小,而 SYSLOG_ACTION_SIZE_UNREAD 则返回当前内核环缓冲区可读取的字符数。表 2 显示了 SYSLOG 命令的完整清单。使用 syslog/klogctl 系统调用实现的命令:#ifndef _LINUX_SYSLOG_H #define _LINUX_SYSLOG_H /* Close the log. Currently a NOP. */ #define SYSLOG_ACTION_CLOSE 0 /* Open the log. Currently a NOP. */ #define SYSLOG_ACTION_OPEN 1 /* Read from the log. */ #define SYSLOG_ACTION_READ 2 /* Read all messages remaining in the ring buffer. */ #define SYSLOG_ACTION_READ_ALL 3 /* Read and clear all messages remaining in the ring buffer */ #define SYSLOG_ACTION_READ_CLEAR 4 /* Clear ring buffer. */ #define SYSLOG_ACTION_CLEAR 5 /* Disable printk's to console */ #define SYSLOG_ACTION_CONSOLE_OFF 6 /* Enable printk's to console */ #define SYSLOG_ACTION_CONSOLE_ON 7 /* Set level of messages printed to console */ #define SYSLOG_ACTION_CONSOLE_LEVEL 8 /* Return number of unread characters in the log buffer */ #define SYSLOG_ACTION_SIZE_UNREAD 9 /* Return size of the log buffer */ #define SYSLOG_ACTION_SIZE_BUFFER 10 #define SYSLOG_FROM_READER 0 #define SYSLOG_FROM_PROC 1 int do_syslog(int type, char __user *buf, int count, int source); #endif /* _LINUX_SYSLOG_H */在实现上面的 syslog/klogctl 层之后,kmsg proc 文件系统成为一个 I/O 通道(在 ./linux/fs/proc/kmsg.c 中实现的),它提供了从内核缓冲区读取日志消息的二进制接口。这个读取操作通常是由一个守护程序(klogd 或 rsyslogd)实现的,它会处理这些消息,然后将它们传递给 rsyslog,以便(基于它的配置)转发到正确的日志文件中。文件 /proc/kmsg 实现了少数等同于内部 do_syslog 的文件操作。在内部,open 调用与 SYSLOG_ACTION_OPEN 有关,而 SYSLOG_ACTION_CLOSE 则与 release 有关(每一个调用都实现为一个 No Operation Performed [NOP])。这个轮循操作会等待文件活动的完成,然后才调用 SYSLOG_ACTION_SIZE_UNREAD 确定可以读取的字符数。最后,read 操作会被映射到 SYSLOG_ACTION_READ,以处理可用的日志消息。注意,用户是不会用到 /proc/kmsg 文件的:守护程序用它来获取日志消息,并将它们转发到 /var 空间内必要的日志文件中。用户空间应用程序用户空间提供了许多读取和管理内核日志的访问方法。我们开始先介绍较底层的接口(如 /proc 文件系统配置元素),然后再介绍更高层的应用程序。/proc 文件系统不仅提供了一个访问日志消息(kmsg)的二进制接口。它还有许多与上面讨论的 syslog/klogctl 相关或无关的配置元素。清单 1 显示了这些参数。清单 1. /proc 中的 printk 配置参数mtj@ubuntu:~$ cat /proc/sys/kernel/printk 4 4 1 7 mtj@ubuntu:~$ cat /proc/sys/kernel/printk_delay 0 mtj@ubuntu:~$ cat /proc/sys/kernel/printk_ratelimit 5 mtj@ubuntu:~$ cat /proc/sys/kernel/printk_ratelimit_burst 10在清单 1 中,第一项定义了 printk API 当前使用的日志级别。这些日志级别表示了控制台的日志级别、默认消息日志级别、最小控制台日志级别和默认控制台日志级别。printk_delay 值表示的是 printk 消息之间的延迟毫秒数(用于提高某些场景的可读性)。注意,这里它的值为 0,而它是不可以通过 /proc 设置的。printk_ratelimit 定义了消息之间允许的最小时间间隔(当前定义为每 5 秒内的某个内核消息数)。消息数量是由 printk_ratelimit_burst 定义的(当前定义为 10)。如果您拥有一个非正式内核而又使用有带宽限制的控制台设备(如通过串口), 那么这非常有用。注意,在内核中,速度限制是由调用者控制的,而不是在 printk 中实现的。如果一个 printk 用户要求进行速度限制,那么该用户就需要调用 printk_ratelimit 函数。dmesg 命令也可用于打印和控制内核环缓冲区。这个命令使用 klogctl 系统调用来读取内核环缓冲区,并将它转发到标准输出(stdout)。这个命令也可以用来清除内核环缓冲区(使用 -c 选项),设置控制台日志级别(-n 选项),以及定义用于读取内核日志消息的缓冲区大小(-s 选项)。注意,如果没有指定缓冲区大小,那么 dmesg 会使用 klogctl 的 SYSLOG_ACTION_SIZE_BUFFER 操作确定缓冲区大小。最后,所有日志应用程序都是基于一个标准化日志框架 syslog,主流操作系统(包括 Linux® 和 Berkeley Software Distribution [BSD])都实现了这个框架。syslog 使用自身的协议实现在不同传输协议的事件通知消息传输(将组件分成发起者、中继者和收集者)。在许多情况中,所有这三种组件都在一个主机上实现。除了 syslog 的许多有意思的特性,它还规定了日志信息是如何收集、过滤和存储的。syslog 已经经过了许多的变化和发展。您可能听过 syslog、klog 或 sysklogd。最新版本的 Ubuntu 使用的是名为 rsyslog(基于原先的 syslog)的新版本 syslog,它指的是可靠的和扩展的 syslogd。syslogd 守护程序通过它的配置文件 /etc/rsyslog.conf 来理解 /proc 文件系统的 kmsg 接口,并使用这些接口获取内核日志消息。注意,在内部,所有日志级别都是通过 /proc/kmsg 写入的,这样所传输的日志级别就不是由内核决定的,而是由 rsyslog 本身决定的。然后这些内核日志消息会存储在 /var/log/kern.log(及其他配置的文件)。在 /var/log 中有许多的日志文件,包括一般消息和系统相调用(/var/log/messages)、系统启动日志(/var/log/boot.log)、认证日志(/var/log/auth.log)等等。虽然您可以检查这些日志,但是您也可以使用它们进行自动审计和检查。有许多日志文件分析器可以用于故障修复, 或者满足安全规范要求,以及自动地使用诸如模式识别或相关性分析(甚至是跨系统的)来发现问题。结束语本文简单地介绍了内核日志和一些应用程序—包括在内核中创建内核日志消息,在内核的环缓冲区存储消息,使用 syslog/klogctl 或 /proc/kmsg 实现到用户空间的消息传输,通过 rsyslog 日志框架实现转发,以及它在 /var/log 子树的最终测试位置。Linux 提供了(包括内核及外部空间的)丰富且灵活的日志框架。
2020年09月15日
1,225 阅读
0 评论
0 点赞
2020-09-11
测试插入b站视频
.aspect-ratio {position: relative;width: 100%;height: 0;padding-bottom: 75%;}.aspect-ratio iframe {position: absolute;width: 100%;height: 100%;left: 0;top: 0;} test 参考: https://blog.csdn.net/xinshou_caizhu/article/details/94028606?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param
2020年09月11日
925 阅读
0 评论
0 点赞
2020-09-11
在qemu上运行ARM
代码仓库在https://github.com/ADTXL/qemu_kernel喜欢的话可以点下star1. 简介学习kenel,使用qemu搭建环境2. 使用说明2.1 编译安装qemu参考qemu/readme.md2.2 安装toolchain本来打算把toolchain也直接上传的,arm64的toolchain有些文件大于100M,不太好直接上传,只上传了arm32的toolchain.如果对版本没有要求可以直接使用命令安装,# 32 bit sudo apt-get install gcc-arm-linux-gnueabihf # 64 bit sudo apt install gcc-aarch64-linux-gnu2.3 编译以编译和运行arm64为例2.3.1 修改路径运行脚本时需要修改脚本run_qemu中和路径"qemuBinPath"和"KernelRootPath"为真实的存在路径2.3.2 编译有些包没有的可能需要安装下,还有些缺少的根据编译报错安装即可,下面是我编译时需要的包sudo apt install bison sudo apt install flex sudo apt install openssl sudo apt install libssl-dev sudo apt install bc然后编译cd buildroot/linux/qemu/arm make -j62.3.3 运行使用如下命令cd buildroot/linux/qemu/arm ./run_qemu.sh如下所示:user@b10fa2dc8f66:~/txl/project/qemu_kernel/buildroot/linux/qemu/arm$ ./run_qemu.sh run qemu without external filesystem mke2fs 1.44.1 (24-Mar-2018) Creating regular file /home/user/txl/project/qemu_kernel/work/linux-qemu-arm-4_19/image/rootfs.ext4 Creating filesystem with 512000 1k blocks and 128016 inodes Filesystem UUID: 61e2e1d6-dbfb-4068-a749-eba9e5bbfe54 Superblock backups stored on blocks: 8193, 24577, 40961, 57345, 73729, 204801, 221185, 401409 Allocating group tables: done Writing inode tables: done Creating journal (8192 blocks): done Copying files into the device: done Writing superblocks and filesystem accounting information: ...... [ 0.794850] devtmpfs: mounted [ 0.969362] Freeing unused kernel memory: 768K [ 0.971016] Run /sbin/init as init process mount: mounting tmpfs on /tmp failed: Invalid argument mount: mounting sdcardfs on /sdcard failed: No such device [ 1.102253] EXT4-fs (vda): re-mounted. Opts: (null) Processing /etc/profile... Done / # ls bin home lost+found root sys usr dev lib mnt sbin system var etc linuxrc proc sdcard tmp 如果想退出,按Ctrl +a,然后再按x即可。3. 目录结构说明TODO
2020年09月11日
860 阅读
0 评论
0 点赞
1
...
36
37
38