Linux内核中常用的C语言技巧(三)

简介: Linux内核中常用的C语言技巧(三)

在现代操作系统架构中,内核空间和用户空间之间增加了一个中间层,这就是系统调用层。

系统调用层主要有如下作用。

为用户空间程序提供一层硬件抽象接口。这能够让应用程序编程者从学习硬件设备底层编程中解放出来。例如,当需要读写一个文件时,应用程序编写者不用去关心磁盘类型和介质,以及文件存储在磁盘哪个扇区等底层硬件信息。

保证系统稳定和安全。应用程序要访问内核必须通过系统调用层,那么内核可以在系统调用层对应用程序的访问权限、用户类型和其他一些规则进行过滤,这样可以避免应用程序不正确地访问内核。

可移植性。可以让应用程序在不修改源代码的情况下,在不同的操作系统或者不同的硬件体系结构的系统中重新编译并且运行。

1 系统调用和POSIX标准

有的读者可能对应用编程接口(API)和系统调用之间的关系有点糊涂了。

一般来说,应用程序调用用户空间实现的应用编程接口来编程,而不是直接调用系统调用。

一个 API接口函数可以由一个系统调用实现,也可以由多个系统调用来实现,甚至完全不使用任何系统调用。

因此,一个API接口没有必要对应一个特定的系统调用。

在UNIX系统设计的早期就出现了操作系统的API接口层。

在UNIX的世界里,最通用的系统调用层接口是POSIX(Portable Operating System Interface of UNIX)标准。POSIX的诞生和UNIX的发展密不可分。

UNIX系统诞生于20世纪70年代的贝尔实验室,很多商业厂商基于UNIX发展自己的UNIX系统,但是标准不统一。后来IEEE制定了POSIX标准,但是需要注意的是,POSIX标准针对的是API而不是系统调用。

判断一个系统是否与POSIX兼容时,要看它是否提供一组合适的应用编程接口,而不是看它的系统调用是如何定义和实现的。

Linux操作系统的API接口通常是以C标准库的方式提供的,比如Linux中的libc库。

C库提供了POSIX的绝大部分的API的实现,同时也为内核提供的每个系统调用封装了相应的函数,并且系统调用和 C 库封装的函数名称通常是相同的。

例如,open 系统调用在 C库的函数也是open函数。

另外几个API函数可能调用封装了不同功能的同一个系统调用,例如,libc库函数中实现的malloc()、calloc()和free()等函数,这几个函数用来分配和释放虚拟内存(堆上的虚拟内存),它们都是利用brk系统调用来实现的。

大家都知道malloc是c中常用的内存操作函数,malloc动态的申请一块指定大小的内存,方便存放数据。而brk/sbrk则是实现malloc的底层函数,其中brk是系统调用。brk和sbrk主要的工作是实现虚拟内存到内存的映射。

每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloc,calloc等)就调用sbrk函数将数据段的下界移动,sbrk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。

sbrk不是系统调用,是C库函数。系统调用通常提供一种小功能,而库函数通常提供比较复杂的功能。sbrk/brk是从堆中分配空间,本质是移动一个位置,向后移就是分配空间,向前移就是释放空间,sbrk用相对的整数值确定位置,如果这个整数是正数,会从当前位置向后移若干字节,如果为负数就向前若干字节。在任何情况下,返回值永远是移动之前的位置。

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

brk是将数据段(.data)的最高地址指针_edata往高地址推;

mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层就是由brk,mmap,munmap这些系统调用实现的。

2 系统调用表

Linux系统为每一个系统调用赋予一个系统调用号。

当应用程序执行一个系统调用时,应用程序就可以知道执行和调用到哪个系统调用了,从而不会造成混乱。

系统调用号一旦分配之后就不会有任何变更,否则已经编译好的应用程序就不能运行了。

对于ARM32系统来说,其系统调用号定义在arch/arm/include/uapi/asm/unistd.h头文件中。

<arch/arm/include/uapi/asm/unistd.h>
/** This file contains the system call numbers.*/
#define __NR_restart_syscall    (__NR_SYSCALL_BASE+ 0)
#define __NR_exit      (__NR_SYSCALL_BASE+ 1)
#define __NR_fork      (__NR_SYSCALL_BASE+ 2)
#define __NR_read      (__NR_SYSCALL_BASE+ 3)
#define __NR_write      (__NR_SYSCALL_BASE+ 4)
#define __NR_open      (__NR_SYSCALL_BASE+ 5)
#define __NR_close      (__NR_SYSCALL_BASE+ 6)
/* 7 was sys_waitpid */
#define __NR_creat      (__NR_SYSCALL_BASE+ 8)
#define __NR_link      (__NR_SYSCALL_BASE+ 9)
#define __NR_unlink     (__NR_SYSCALL_BASE+ 10)
#define __NR_execve     (__NR_SYSCALL_BASE+ 11)
#define __NR_chdir      (__NR_SYSCALL_BASE+ 12)
#define __NR_time      (__NR_SYSCALL_BASE+ 13)
#define __NR_mknod      (__NR_SYSCALL_BASE+ 14)
#define __NR_chmod      (__NR_SYSCALL_BASE+ 15)
#define __NR_lchown     (__NR_SYSCALL_BASE+ 16)

例如,open这个系统调用被赋予的号码是5,因此在所有的ARM32系统中,这个open系统调用号是不能被更改的。

open系统调用最终的实现在如下函数中。

<fs/open.c>
SYSCALL_DEFINE3(open, const char __user *, filename,int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}

SYSCALL_DEFINE是一个宏,其实现是在include/linux/syscalls.h头文件中。

<include/linux/syscalls.h>
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1,_##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2,_##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3,_##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4,_##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5,_##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6,_##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, sname, ...)        \
SYSCALL_METADATA(sname, x, __VA_ARGS__)      \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

该宏最后扩展完会变成sys_open()函数。

asmlinkage long sys_open(const char __user *filename,int flags, umode_t mode);

3 用程序访问系统调用

应用程序编写者通常不会直接访问系统调用,而是通过C标准库函数来访问系统调用。

如果给Linux系统新添加了一个系统调用,那么可以通过直接调用syscall()函数来访问新添加的系统调用。

#include <unistd.h>
#include <sys/syscall.h> 
/* 系统调用定义 */
long syscall(long number, ...);

syscall()函数可以直接调用一个系统调用,第一个参数是系统调用号码,比如上面提到的open系统调用号码是5;

“…”是可变参数,用来传递参数到内核。

以上述的open系统调用为例,在应用程序中可以用如下代码直接调用。

#define NR_OPEN 5
syscall(NR_OPEN, filename, flags, mode);

4 新增系统调用

读者可能疑惑,既然Linux系统为我们提供了几百个系统调用,当我们在实际项目中遇到问题时,是否可以新增一个系统调用呢?

在Linux系统中新增一个系统调用是很容易的事情,但是我们不提倡新增系统调用,因为新增一个系统调用意味着你的应用程序可能缺乏了移植性。

Linux 系统的系统调用必须由 Linux 社区来决定,并且和 glibc 社区同步,也就是需要Linux和glibc同步进行修改。

因此,新增一个系统调用需要在社区里充分讨论和沟通,这个过程会非常漫长。

其实Linux内核里提供了很多机制来让用户程序和内核进行信息交互,读者应该充分思考是否可以使用如下方法来实现,而不是考虑新增一个系统调用。

设备节点。实现一个设备节点之后,就可以对该设备进行read()和write()等操作,甚至可以通过ioctl()接口来自定义一些操作。

sysfs接口。sysfs接口也是一种推荐的用户程序和内核直接的通信方式,这种方式很灵活,也是Linux内核推荐的做法。还有proc。

参考资料

《奔跑吧Linux内核》


目录
相关文章
|
4天前
|
安全 Linux 测试技术
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
|
17天前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
65 15
|
1月前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
1月前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
1月前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
1月前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
1月前
|
负载均衡 算法 Linux
深入探索Linux内核调度机制:公平与效率的平衡####
本文旨在剖析Linux操作系统内核中的进程调度机制,特别是其如何通过CFS(完全公平调度器)算法实现多任务环境下资源分配的公平性与系统响应速度之间的微妙平衡。不同于传统摘要的概览性质,本文摘要将直接聚焦于CFS的核心原理、设计目标及面临的挑战,为读者揭开Linux高效调度的秘密。 ####
44 3
|
2月前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
49 6
|
1月前
|
消息中间件 安全 Linux
深入探索Linux操作系统的内核机制
本文旨在为读者提供一个关于Linux操作系统内核机制的全面解析。通过探讨Linux内核的设计哲学、核心组件、以及其如何高效地管理硬件资源和系统操作,本文揭示了Linux之所以成为众多开发者和组织首选操作系统的原因。不同于常规摘要,此处我们不涉及具体代码或技术细节,而是从宏观的角度审视Linux内核的架构和功能,为对Linux感兴趣的读者提供一个高层次的理解框架。
|
2月前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
59 1