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内核》


目录
相关文章
|
8天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
41 4
|
12天前
|
缓存 算法 Linux
深入理解Linux内核调度器:公平性与性能的平衡####
真知灼见 本文将带你深入了解Linux操作系统的核心组件之一——完全公平调度器(CFS),通过剖析其设计原理、工作机制以及在实际系统中的应用效果,揭示它是如何在众多进程间实现资源分配的公平性与高效性的。不同于传统的摘要概述,本文旨在通过直观且富有洞察力的视角,让读者仿佛亲身体验到CFS在复杂系统环境中游刃有余地进行任务调度的过程。 ####
33 6
|
3天前
|
算法 Linux 开发者
Linux内核中的锁机制:保障并发控制的艺术####
本文深入探讨了Linux操作系统内核中实现的多种锁机制,包括自旋锁、互斥锁、读写锁等,旨在揭示这些同步原语如何高效地解决资源竞争问题,保证系统的稳定性和性能。通过分析不同锁机制的工作原理及应用场景,本文为开发者提供了在高并发环境下进行有效并发控制的实用指南。 ####
|
11天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
38 9
|
10天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
31 6
|
11天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
30 5
|
11天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
12天前
|
存储 监控 安全
Linux内核调优的艺术:从基础到高级###
本文深入探讨了Linux操作系统的心脏——内核的调优方法。文章首先概述了Linux内核的基本结构与工作原理,随后详细阐述了内核调优的重要性及基本原则。通过具体的参数调整示例(如sysctl、/proc/sys目录中的设置),文章展示了如何根据实际应用场景优化系统性能,包括提升CPU利用率、内存管理效率以及I/O性能等关键方面。最后,介绍了一些高级工具和技术,如perf、eBPF和SystemTap,用于更深层次的性能分析和问题定位。本文旨在为系统管理员和高级用户提供实用的内核调优策略,以最大化Linux系统的效率和稳定性。 ###
|
11天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
11天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
27 2