大家好,我是小康 ,今天我们来学习一下 Linux 系统编程相关的知识。Linux 系统编程是连接高级语言和硬件的桥梁,它对深入理解计算机系统至关重要。无论你是打算构建高性能服务器还是开发嵌入式设备,掌握 Linux 系统编程是 C 和 C++ 开发者的基本技能。
本文旨在为初学者提供一个清晰的 Linux 系统编程入门指南,带你步入 Linux 系统编程的世界,从基本概念到实用技能,一步步建立起您的知识体系。
基本概念
什么是系统编程?
系统编程,指的是开发那些直接与计算机硬件或操作系统进行交互的程序。这些程序负责管理和控制计算机系统的资源,包括但不限于进程、内存、文件系统和设备驱动。确保为应用程序提供一个稳定、高效的运行环境。
系统编程与应用编程的主要区别:
Linux系统编程核心技术概览
在电脑的世界中,操作系统起到桥梁的作用,连接用户与计算机硬件。其中,Linux 由于其开源、稳定和安全的特点,成为了许多工程师的首选。为了更深入地理解它,我们首先需要了解其系统架构的神秘面纱。
Linux 系统架构解析
用户空间和内核空间的布局
各个内核组件说明:
用户空间 (User Space)
所有的应用程序,如浏览器、文档编辑器或音乐播放器都运行在这个空间。
内核空间 (Kernel Space)
内核空间是操作系统的核心。
系统调用与库函数
在 Linux 编程中,我们经常听到“系统调用”和“库函数”这两个词,但你知道它们之间的区别吗?接下来就让我们来详细了解一下。
什么是系统调用?
系统调用是一个程序向操作系统发出的请求。当应用程序需要访问某些资源(如磁盘、网络或其他硬件设备)或执行某些特定的操作(如创建进程或线程)时,它通常会通过系统调用来完成。
工作原理
常见的系统调用函数:
read() 和 write():分别用于读取和写入文件。
open() 和 close():打开和关闭文件。
fork():创建一个新的进程。
wait():等待进程结束。
exec():执行一个新程序。
这只是系统调用的冰山一角。Linux 提供了上百个系统调用,每个都有其特定的功能。
什么是库函数?
库函数是预编写的代码,存储在库文件中,供程序员使用。它们通过系统调用和操作系统的内核通信。例如,printf() 是 C 语言的一个库函数,它内部使用 write() 系统调用来和内核进行交互。
文件 IO
文件IO(输入/输出)是计算机程序与文件系统交互的基本方式,允许程序读取和写入文件。要深入理解和使用文件IO,首先需要了解一些关键概念和操作。
文件描述符是什么?
文件描述符「 fd 」是一个整数,它代表了一个打开的文件。在 Linux 中,每次我们打开或创建一个文件时,系统都会返回一个文件描述符。而应用程序正是通过这个文件描述符「 fd 」来进行文件的读写的。
特殊的文件描述符:
常见的文件操作
当应用程序要与文件交互时,最基本的操作包括打开、读取、写入和关闭文件。这可以通过以下函数来实现。
打开文件: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);
文件锁定
什么是文件锁定?
文件锁定是一个在多个进程或线程之间协调对共享资源访问的机制。在这里,这个"共享资源"指的是文件。简单说,文件锁就是确保当一个进程正在使用一个文件时,其他进程不能修改它。
为什么需要文件锁定?
考虑这样一个场景:两个程序同时写入一个文件。不锁定文件可能会导致数据混乱。例如,一个进程可能会覆盖另一个进程的更改。所以,文件锁定是确保数据完整性的关键。
文件锁的两种模式:
如何实现文件锁定?
在 Linux 编程中,文件锁定可以使用以下函数实现:
fcntl() : 允许对文件中的特定部分进行锁定。
flock() :提供了一个简化的锁定机制,直接锁定整个文件。
重定向
什么是重定向?
重定向,顾名思义,指的是改变数据流的方向。在 Linux 系统编程中,程序通常与三种标准I/O 流进行交互:标准输入(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 系列函数 允许一个进程运行另一个程序,它实际上替换了当前进程的内容。
进程的状态转换图
五态简要说明:
进程间通信
在 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) 是一种特殊类型的文件,用于在不相关的进程之间实现通信。与匿名管道不同,有名管道在文件系统中具有一个实际的路径名。这允许任何具有适当权限的进程打开和使用它,而不仅限于有亲缘关系的进程。