Linux 系统编程从入门到进阶 学习指南
创始人
2024-11-07 18:58:54

大家好,我是小康 ,今天我们来学习一下 Linux 系统编程相关的知识。Linux 系统编程是连接高级语言和硬件的桥梁,它对深入理解计算机系统至关重要。无论你是打算构建高性能服务器还是开发嵌入式设备,掌握 Linux 系统编程是 C 和 C++ 开发者的基本技能。

本文旨在为初学者提供一个清晰的 Linux 系统编程入门指南,带你步入 Linux 系统编程的世界,从基本概念到实用技能,一步步建立起您的知识体系。

基本概念

什么是系统编程?

系统编程,指的是开发那些直接与计算机硬件或操作系统进行交互的程序。这些程序负责管理和控制计算机系统的资源,包括但不限于进程、内存、文件系统和设备驱动。确保为应用程序提供一个稳定、高效的运行环境。

系统编程与应用编程的主要区别

  • 目的性:系统编程旨在为计算机或操作系统本身提供功能和服务,而应用编程是为了满足最终用户的特定需求。
  • 交互对象:系统编程直接与硬件或操作系统交互,而应用编程与操作系统或其他应用交互。
  • 复杂性:由于系统编程需要管理和控制计算机的底层资源,因此通常比应用编程更为复杂。
  • 开发工具:系统编程通常使用低级语言,如 C 或汇编,因为这些语言提供了直接访问硬件的能力。而应用编程可能使用更高级的语言,如 Python 或 Java,以提高开发效率。

Linux系统编程核心技术概览

在电脑的世界中,操作系统起到桥梁的作用,连接用户与计算机硬件。其中,Linux 由于其开源、稳定和安全的特点,成为了许多工程师的首选。为了更深入地理解它,我们首先需要了解其系统架构的神秘面纱。

Linux 系统架构解析

用户空间和内核空间的布局

各个内核组件说明:

  • 系统调用 (Syscalls)
  • 当应用程序需要访问硬件资源时,它们使用系统调用来与内核通信。
  • 进程管理
  • 负责处理进程创建、调度和终止。确保系统中的进程公平、有效地获得 CPU 时间,并管理进程间的通信和同步。
  • 内存管理
  • 管理物理内存,提供虚拟内存和分页功能。确保每个进程都有它自己的地址空间,同时保护进程间的内存不被非法访问。
  • 文件系统
  • 提供文件和目录的创建、读取、写入和删除功能。它抽象了物理存储设备,为用户和应用程序提供了一个统一的文件访问接口。
  • 虚拟文件系统(VFS)
  • 用户和应用程序不直接与各种文件系统交互。而是通过 VFS(虚拟文件系统)进行操作。VFS为各种不同的文件系统(如EXT4, FAT, NFS等)提供一个统一的接口。这样,无论底层使用的是哪种文件系统,用户和应用的文件访问方式都保持一致,实现在 Linux 中的无缝集成。
  • 网络协议栈
  • 负责处理计算机之间的通信,使设备能够在网络上发送和接收数据。它包含了多层协议,如 TCP/IP,使计算机能够连接到互联网和其他网络,并与其他计算机进行数据交换。
  • 设备驱动
  • 设备驱动是一种特殊的软件程序,它允许 Linux 内核和计算机的硬件组件进行交互。这些硬件组件可以是任何物理设备,如显卡、声卡、网络适配器、硬盘或其他输入/输出设备。设备驱动为硬件设备提供了一个抽象层,使得内核和应用程序不需要知道硬件的具体细节,就能与其进行通信和控制。简而言之,设备驱动是硬件和操作系统之间通信的桥梁。

用户空间 (User Space)

所有的应用程序,如浏览器、文档编辑器或音乐播放器都运行在这个空间。

  • 安全性:用户空间的程序运行在受限的环境中,它们只能访问分配给它们的资源,不能直接访问硬件或其他程序的数据。
  • 稳定性:如果一个应用程序崩溃,它不会影响其他应用程序或系统的核心功能。

内核空间 (Kernel Space)

内核空间是操作系统的核心。

  • 权限:内核可以直接访问硬件,并有权执行任何命令。
  • 安全性:虽然内核拥有广泛的权限,但只有那些已知且经过严格测试和验证的代码才被允许在内核空间执行。
  • 稳定性:如果内核遇到问题,整个系统可能会崩溃。

系统调用与库函数

Linux 编程中,我们经常听到“系统调用”和“库函数”这两个词,但你知道它们之间的区别吗?接下来就让我们来详细了解一下。

什么是系统调用?

系统调用是一个程序向操作系统发出的请求。当应用程序需要访问某些资源(如磁盘、网络或其他硬件设备)或执行某些特定的操作(如创建进程或线程)时,它通常会通过系统调用来完成。

工作原理

  • 模式切换:应用程序在用户空间运行,而操作系统内核在内核空间运行。系统调用涉及从用户空间切换到内核空间。
  • 参数传递:程序将参数传递给系统调用,通常通过特定的寄存器。
  • 执行:内核根据传递的参数执行相应的操作。
  • 返回结果:操作完成后,内核将结果返回给应用程序,并将控制权返回给应用程序。

常见的系统调用函数:

read() 和 write():分别用于读取和写入文件。

open() 和 close():打开和关闭文件。

fork():创建一个新的进程。

wait():等待进程结束。

exec():执行一个新程序。

这只是系统调用的冰山一角。Linux 提供了上百个系统调用,每个都有其特定的功能。

什么是库函数?

库函数是预编写的代码,存储在库文件中,供程序员使用。它们通过系统调用和操作系统的内核通信。例如,printf() 是 C 语言的一个库函数,它内部使用 write() 系统调用来和内核进行交互。

文件 IO

文件IO(输入/输出)是计算机程序与文件系统交互的基本方式,允许程序读取和写入文件。要深入理解和使用文件IO,首先需要了解一些关键概念和操作。

文件描述符是什么?

文件描述符「 fd 」是一个整数,它代表了一个打开的文件。在 Linux 中,每次我们打开或创建一个文件时,系统都会返回一个文件描述符。而应用程序正是通过这个文件描述符「 fd 」来进行文件的读写的。

特殊的文件描述符:

  • 标准输入「stdin」 是 0
  • 标准输出「stdout」 是 1
  • 标准错误 「stderr」 是 2

常见的文件操作

当应用程序要与文件交互时,最基本的操作包括打开、读取、写入和关闭文件。这可以通过以下函数来实现。

打开文件:open()

读取文件:read()

写入文件:write()

关闭文件:close()

# demo

int fd = open("example.txt", O_RDWR | O_CREAT);

write(fd, "Hello, File!", 12);

close(fd);

文件位置与移动

有时,我们可能需要移动到文件的特定位置进行读写。使用 lseek() 可以实现这一点。举个例子:

/*

假设我们有一个名为 "data.txt" 的文件,内容为:Hello World!

现在我们有一个简单需求:我们想将文件中的"World"替换为"Linux",但不想重写整个文件。

*/

# demo 展示:

char buffer[6]; // 存放从文件中读取的数据

int fd = open("data.txt", O_RDWR); # 以读写模式打开文件

lseek(fd, 6, SEEK_SET); // 使用 lseek() 移动到"World"的开头位置

read(fd, buffer, 5); // 读取5个字符("World"的长度)

if (strcmp(buffer, "World") == 0) {

// 重新定位文件指针以替换"World",这里需要重新定位的原因是:上面 read 操作使得文件指针已经指向文件末尾了,因此需要重新定位。

lseek(fd, 6, SEEK_SET);

write(fd, "Linux", 5);

}

close(fd) ;

高级文件 I/O

有时,简单的读写操作无法满足我们的需求,尤其当我们追求高效率或特殊功能时。为了更优雅、高效地处理文件数据,我们引入了一些高级文件 I/O 技巧。

分散读取和集中写入

#include

// 读取操作

ssize_t readv(syfsmy.com fd, vpxhome.com struct iovec *iov, int iovcnt);

// 写入操作

ssize_t writev(molyang.com fd, const struct iovec *iov, int iovcnt);

# iovec 结构的定义如下:

struct iovec {

void *iov_base;

size_t iov_len;

};

iov_base 是指向缓冲区起始地址的指针。

iov_len 是缓冲区的大小。

这两个函数主要用于多缓冲区的输入/输出操作,允许您在单次系统调用中,从文件读取到多个缓冲区或从多个缓冲区写入文件。

它们的主要目的是提高效率,因为常规的读/写函数每次只能在一个缓冲区进行操作。

内存映射文件I/O

内存映射文件 I/O 允许程序员将文件的一部分直接映射到进程的内存中。这样,程序可以通过直接访问这块内存来访问文件的内容,而不是使用传统的 read 、write 系统调用。这可以提高效率,特别是对于大文件的访问。

#include

// 相关函数声明

void* mmap(bbhbd.com* addr, size_t length, int prot, int flags, int fd, off_t offset);

int munmap(lszhsc.com* addr, size_t length);

// demo 举例:

int fd = open("example.txt", O_RDWR);

// 获取文件的大小

struct stat sb;

if (fstat(fd, &sb) == -1) {

perror("fstat");

}

char *mapped = mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 后续的所有对文件的操作就可以通过 mapped 指针来进行。

// 例如:将第一个字符改为 'J')

mapped[0] = 'J';

使用 mmap ,你可以直接在内存中访问文件内容,如同访问数组或其他数据结构一样。

同步文件操作

当您向文件写入数据时,操作系统可能会缓存这些数据,而不是立即写入磁盘,这样可以提高效率。 但在某些情况下,您可能需要确保数据确实已经写入磁盘。这就是同步文件操作的用处。

#include

#include

int msync(void *addr, size_t length, int flags);

int fsync(int fd);

int fdatasync(int fd);

void m.fenglinggame.com(void);

  • msync 用于同步内存映射(通过 mmap 函数创建)文件的内容。它将内存中的更改写回到映射的文件中。
  • fsync 函数用于将指定文件描述符(fd)关联的文件的所有修改(包括数据和元数据)同步到磁盘
  • fdatasync 函数类似于 fsync,但它只同步文件的数据部分,而不同步元数据。
  • sync 同步整个文件系统的所有修改的数据到磁盘,包括所有打开的文件。

文件锁定

什么是文件锁定?

文件锁定是一个在多个进程或线程之间协调对共享资源访问的机制。在这里,这个"共享资源"指的是文件。简单说,文件锁就是确保当一个进程正在使用一个文件时,其他进程不能修改它。

为什么需要文件锁定?

考虑这样一个场景:两个程序同时写入一个文件。不锁定文件可能会导致数据混乱。例如,一个进程可能会覆盖另一个进程的更改。所以,文件锁定是确保数据完整性的关键。

文件锁的两种模式

  • 共享锁(Shared Locks):也被称为读锁。当一个进程持有共享锁时,其他进程可以获得该文件的共享锁以进行读取,但不能获得独占锁进行写入。
  • 独占锁(Exclusive Locks):也被称为写锁。当一个进程持有独占锁时,其他进程不能获得该文件的任何类型的锁。这意味着其他进程不可以读取或写入该文件。

如何实现文件锁定?

在 Linux 编程中,文件锁定可以使用以下函数实现:

fcntl() : 允许对文件中的特定部分进行锁定。

flock() :提供了一个简化的锁定机制,直接锁定整个文件。

重定向

什么是重定向?

重定向,顾名思义,指的是改变数据流的方向。在 Linux 系统编程中,程序通常与三种标准I/O 流进行交互:标准输入(stdin)、标准输出(stdout)、和标准错误输出(stderr)。

  • 标准输入(stdin) : 来自键盘的输入。
  • 标准输出(stdout) : 显示到屏幕上。
  • 标准错误输出(stderr) : 也显示到屏幕上。

重定向的核心是将这些标准的 I/O 流改变到其他地方,如文件或其他程序。

例如,当我们在命令行中执行命令并将结果保存到文件中,或者从文件中获取命令的输入而不是从键盘中获取,我们都是在使用重定向。

# 将 ls -l 命令的输出(即当前目录的详细列表)重定向到 filelist.txt 文件中

ls -l > filelist.txt

重定向不仅局限于命令行界面,它在程序中也很有用,允许我们动态地更改程序的输入和输出来源,为构建更复杂、灵活的应用程序提供了基础。

Linux 系统编程中,实现重定向的一个核心函数是 dup2 函数。

#include

int dup2(int oldfd, int newfd);

/*

其中:

oldfd 是原始文件描述符。

newfd 是要复制到的目标文件描述符。

*/

# demo 举例:

int main() {

// 打开一个文件用于写入

int file_023youpuda.com = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

if (file_fd < 0) {

// 错误处理

}

// 使用 dup2 将标准输出重定向到文件

if (dup2(file_fd, STDOUT_FILENO) < 0) {

// 错误处理

}

// 现在,所有标准输出都会被写入文件

printf("This ycgtyy120.com be written to the file 'output.txt'\n");

close(file_fd);

return 0;

}

Linux 进程

你有没有想过,当你在 Linux 操作系统上运行一个程序时,都发生了哪些神奇的事情?接下来,我们将一步一步地深入探讨 Linux 进程的世界。

进程究竟是什么?

每当你启动一个程序,Linux 系统都会创建一个新的进程。这个进程有它自己的内存地址、系统资源和状态。简而言之,进程是程序的一个运行实例。

进程的创建和终止

fork():当调用 fork 函数时,它会创建一个新的子进程。这个子进程几乎是父进程的复制品,包括父进程的内存、程序计数器等。

wait() & waitpid():这些函数允许父进程等待子进程的结束,并收集子进程的退出状态。防止出现僵尸进程。

exec() 系列函数:exec 系列函数 允许一个进程运行另一个程序,它实际上替换了当前进程的内容。

进程的状态转换图

五态简要说明:

  • 新建状态: 这是进程刚被创建时的状态。在这个状态下,操作系统为进程分配了一个唯一的进程标识符(PID)和必要的资源。但进程还没有开始执行任何代码。新建状态通常非常短暂,用户很难观察到,因为进程很快就会转移到 「就绪状态」
  • 就绪状态 : 进程已准备好运行并等待操作系统的调度器分配 CPU 时间片。在这个状态下,进程已经加载了所有必要的代码和数据到内存中,且已准备好执行。
  • 运行状态 : 进程正在 CPU 上执行。一个进程只有在运行状态时才能执行其指令。
  • 阻塞状态 : 进程不能执行,因为它在等待一些事件发生,例如 I/O 操作的完成、信号的接收等。在此状态下,即使 CPU 空闲,进程也不能执行。
  • 终止状态 : 进程已完成执行或被终止。在这个状态下,进程的资源通常被回收,进程退出。

进程间通信

在 Linux 的世界里,进程是操作系统进行资源分配的基本单位。但是,进程并不是孤立的存在。当你的应用分成多个独立运行的进程时,这些进程之间如何有效地交换信息呢?这正是通过进程间通信的方式来实现的。

Linux 提供了以下几种进程间通信的方式

1.管道 (Pipe)

管道是 Linux 中用于进程间通信的一种机制。它们分为两种类型:匿名管道有名管道

匿名管道 :

概念:匿名管道是一种在有亲缘关系的进程间(如父子进程)进行单向数据传输的通信机制,存在于内存中,通常用于临时通信。如果需要双向通信,则一般需要两个管道。

简单图解:

使用场景:适用于有亲缘关系的进程间的简单数据传输。

简单示例:

#include

int main() {

int pipefd[2];

pipe(pipefd); // 创建匿名管道

if (fork() == 0) { // 子进程

close(pipefd[1]); // 关闭写端

//读取数据

read(pipefd[0],buf,5);

// ...

} else { // 父进程

close(pipefd[0]); // 关闭读端

// 写入数据

write(pipefd[1],"hello",5);

// ...

}

}

有名管道

概念: 有名管道(FIFO,First-In-First-Out) 是一种特殊类型的文件,用于在不相关的进程之间实现通信。与匿名管道不同,有名管道在文件系统中具有一个实际的路径名。这允许任何具有适当权限的进程打开和使用它,而不仅限于有亲缘关系的进程。

相关内容

热门资讯

迪丽热巴“疯刀”美学封神!这才... 播出一小时热度便破25000.《枭起青壤》是当之无愧的年底大爆款。从已播的前几集来看,这部剧延续了原...
网红非洲疑遭同胞绑架后续!正主... 自媒体时代,流量为主,所以目前的状况静观其变才是最合适的。11月28号,诸多媒体报道了一则令人愤慨的...
中国女足0-8不敌英格兰女足 直播吧11月30日讯 友谊赛,中国女足0-8惨败英格兰女足。而在整整10年前,中国女足也与英格兰女足...
华为AI玩具“智能憨憨”开售即... 《科创板日报》11月29日讯(记者 黄心怡)在本周的华为Mate80系列发布会上,情感陪聊AI玩具“...
《疯狂动物城2》里有哪些细节 暌违九年,最强跨物种搭(C)档(P)终于又在大荧幕上登场了!作为看动物主演电影常态性关注点跑偏的灵魂...