UNIX环境高级编程(中文版)


本文内容来源于精品资源《UNIX环境高级编程(中文版)》,点击前往

简介:
《UNIX环境高级编程》是一本深受程序员和系统管理员喜爱的经典之作,它由W. Richard Stevens撰写,对中国乃至全球的UNIX和Linux开发者具有深远影响。这本书深入浅出地讲解了在UNIX系统上进行程序开发的各种技术和实践,对于理解操作系统内核与用户空间之间的交互,以及如何高效、稳定地编写系统级程序至关重要。

本书涵盖了UNIX系统的基本概念,包括进程管理、文件系统、I/O操作、信号处理等核心主题。通过学习,读者可以掌握如何创建和管理进程,了解进程间通信机制如管道、套接字和共享内存,以及如何利用fork和exec函数家族来实现进程的创建和替换。

书中详尽解析了文件系统的工作原理和编程接口,包括打开、关闭、读写文件的API,以及目录操作和文件权限管理。这对于编写涉及文件操作的任何程序都是必不可少的知识。

再者,I/O操作是UNIX编程的重要组成部分。书中详细阐述了低级I/O(如read和write)与高级I/O(如stdio库)的区别,以及异步I/O和缓冲技术的应用。同时,还介绍了标准I/O流和磁盘I/O的实现方式,帮助开发者理解和优化程序性能。

信号处理是UNIX环境中用于进程协调的关键机制。书中的这一部分介绍了各种信号类型、信号处理函数以及如何编写信号安全的代码,这对于编写可靠和响应迅速的程序至关重要。

此外,书中还涵盖了网络编程的基础知识,如套接字API的使用,以及TCP/IP协议栈的原理。这部分内容对现代互联网应用的开发者尤其重要,因为它提供了在网络环境下进行通信的工具和方法。

书中还探讨了诸如进程环境、进程组和会话、终端管理、标准I/O重定向等高级话题,这些内容对于编写脚本和管理系统服务非常实用。

《UNIX环境高级编程》是一部全面且深入的教程,不仅适合初学者建立坚实的UNIX编程基础,也对有经验的开发者提供了一种深入理解系统内部运作的宝贵资源。无论你是想提升系统编程技能,还是解决实际工作中的难题,这本书都将是你不可或缺的参考书籍。通过阅读和实践书中的示例,你将能够更熟练地驾驭UNIX和Linux环境,编写出高效、可靠的软件。


这是一张图片

第一章 引言

1.1 UNIX与Linux的历史背景

UNIX操作系统起源于1969年,由贝尔实验室的Ken Thompson、Dennis Ritchie等人开发。它的设计理念是简洁、模块化和可移植性,迅速成为学术界和工业界的标准操作系统。随着时间的推移,UNIX的多个变种相继出现,其中Linux是最为著名的一个。Linus Torvalds于1991年发布了第一个Linux内核,随后Linux迅速发展成为一个开源的、广泛使用的操作系统,尤其在服务器和嵌入式系统中占据了重要地位。

UNIX和Linux的设计哲学强调工具的组合与重用,程序应当做一件事情并做好。这种理念使得UNIX和Linux成为了开发者和系统管理员的理想选择,尤其是在系统级编程和网络编程领域。

1.2 《UNIX环境高级编程》的重要性

《UNIX环境高级编程》是W. Richard Stevens撰写的一本经典著作,深入探讨了在UNIX环境下进行系统级编程的各种技术和实践。该书不仅适合初学者建立坚实的UNIX编程基础,也为有经验的开发者提供了深入理解系统内部运作的宝贵资源。

书中涵盖了进程管理、文件系统、I/O操作、信号处理等核心主题,帮助读者掌握如何高效、稳定地编写系统级程序。通过学习本书,开发者能够更好地理解操作系统的工作原理,从而编写出高效、可靠的软件。

1.3 目标读者与学习方法

本书的目标读者包括:

  • 初学者:希望建立UNIX编程基础的学生和新手开发者。
  • 中级开发者:希望提升系统编程技能的程序员。
  • 系统管理员:需要理解UNIX/Linux系统内部运作的IT专业人士。

学习方法

  1. 理论结合实践:在学习每个章节的理论知识后,务必进行相应的实践,编写代码并进行调试。
  2. 阅读代码示例:书中提供了大量代码示例,读者应仔细阅读并理解每个示例的实现原理。
  3. 动手实验:在自己的UNIX/Linux环境中进行实验,尝试修改示例代码并观察结果。
  4. 参与社区:加入UNIX/Linux相关的开发者社区,分享经验和解决问题。

通过以上方法,读者将能够更深入地理解UNIX环境下的编程技巧,为后续章节的学习打下坚实的基础。接下来,我们将深入探讨UNIX系统的基础知识,为后续的编程实践做好准备。

第二章 UNIX系统基础

2.1 UNIX系统架构概述

UNIX系统的架构主要由内核和用户空间两部分组成。内核是操作系统的核心,负责管理系统资源、进程调度、内存管理和设备控制等。用户空间则是用户程序运行的环境,用户在这里执行应用程序并与系统进行交互。

内核的功能

  • 进程管理:负责创建、调度和终止进程。
  • 内存管理:管理系统内存的分配和回收。
  • 文件系统管理:提供文件的创建、删除、读写等操作。
  • 设备管理:控制硬件设备的输入输出操作。

用户空间的功能

用户空间包含用户应用程序和库,用户通过系统调用与内核进行交互。用户空间的程序通常是以进程的形式运行,每个进程都有自己的地址空间和资源。

2.2 用户空间与内核空间的交互

用户空间与内核空间之间的交互主要通过系统调用实现。系统调用是用户程序请求内核服务的接口,常见的系统调用包括文件操作、进程控制和网络通信等。

示例:使用系统调用创建文件

以下是一个简单的C语言示例,展示如何使用系统调用在UNIX系统中创建一个文件。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    // 创建一个新文件,文件名为example.txt,权限为0644
    int fd = open("example.txt", O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    
    // 检查文件是否成功创建
    if (fd == -1) {
        perror("Error creating file");
        return 1;
    }

    // 写入内容到文件
    const char *text = "Hello, UNIX!\n";
    ssize_t bytes_written = write(fd, text, 14);
    
    // 检查写入是否成功
    if (bytes_written == -1) {
        perror("Error writing to file");
        close(fd);
        return 1;
    }

    // 关闭文件描述符
    close(fd);
    printf("File created and written successfully.\n");
    return 0;
}

代码总结

在上述代码中,我们使用open系统调用创建了一个名为example.txt的文件,并设置了文件的权限。接着,我们使用write系统调用将字符串“Hello, UNIX!”写入文件。最后,通过close系统调用关闭文件描述符,释放资源。

结果说明

运行该程序后,会在当前目录下创建一个名为example.txt的文件,文件内容为“Hello, UNIX!”。如果在创建或写入文件时发生错误,程序会输出相应的错误信息。

2.3 进程与线程的基本概念

在UNIX系统中,进程是资源分配的基本单位,而线程是进程内的执行单元。每个进程都有自己的地址空间、数据栈和其他辅助数据,而线程则共享进程的资源。

进程的特点

  • 独立性:每个进程都有独立的内存空间。
  • 资源分配:进程是系统资源的基本分配单位。
  • 调度:操作系统负责进程的调度与管理。

线程的特点

  • 轻量级:线程的创建和销毁开销小。
  • 共享资源:同一进程内的线程共享进程的资源。
  • 并发执行:多个线程可以并发执行,提高程序的效率。

示例:创建进程

以下是一个简单的C语言示例,展示如何使用fork系统调用创建一个新进程。

#include <stdio.h>
#include <unistd.h>

int main() {
    // 创建新进程
    pid_t pid = fork();

    if (pid < 0) {
        // fork失败
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("This is the child process with PID: %d\n", getpid());
    } else {
        // 父进程
        printf("This is the parent process with PID: %d and child PID: %d\n", getpid(), pid);
    }

    return 0;
}

代码总结

在上述代码中,我们使用fork系统调用创建了一个新进程。fork返回值的不同表示当前进程是父进程还是子进程。父进程会输出自己的PID和子进程的PID,而子进程则输出自己的PID。

结果说明

运行该程序后,您将看到父进程和子进程的输出,显示它们各自的PID。这表明成功创建了一个新进程。

小结

本章介绍了UNIX系统的基本架构,包括内核与用户空间的关系、系统调用的使用以及进程与线程的基本概念。通过理解这些基础知识,读者将能够更好地进行系统级编程,并为后续章节的学习打下坚实的基础。接下来,我们将深入探讨进程管理的相关内容。

第三章 进程管理

3.1 进程的创建与终止

在UNIX系统中,进程是执行中的程序的实例。每个进程都有自己的地址空间、数据栈和其他辅助数据。进程的创建通常通过fork系统调用实现,而进程的终止则通过exit系统调用完成。

进程创建

fork系统调用用于创建一个新进程。调用fork后,操作系统会复制当前进程的所有资源,包括内存、文件描述符等。新创建的进程称为子进程,原进程称为父进程。

进程终止

进程可以通过调用exit系统调用正常终止,或者通过其他进程的kill系统调用被强制终止。终止进程后,操作系统会释放该进程占用的资源。

示例:创建与终止进程

以下是一个简单的C语言示例,展示如何创建一个子进程并在子进程中正常终止。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork(); // 创建新进程

    if (pid < 0) {
        // fork失败
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is running.\n", getpid());
        // 子进程执行完毕,正常退出
        _exit(0); // 使用_exit避免调用父进程的atexit处理
    } else {
        // 父进程
        printf("Parent process (PID: %d) is waiting for child process to finish.\n", getpid());
        wait(NULL); // 等待子进程结束
        printf("Child process has finished.\n");
    }

    return 0;
}

代码总结

在上述代码中,我们使用fork创建了一个子进程。子进程输出自己的PID并正常退出,而父进程则等待子进程结束并输出相关信息。注意,使用_exit而不是exit,以避免父进程的atexit处理被调用。

结果说明

运行该程序后,您将看到父进程和子进程的输出,表明子进程成功创建并正常终止。父进程在子进程结束后继续执行。

3.2 fork与exec函数详解

forkexec是UNIX系统中两个重要的系统调用。fork用于创建新进程,而exec用于在当前进程中执行新的程序。

fork的工作原理

fork调用后,操作系统会复制当前进程的所有资源,创建一个新的进程。新进程的PID与父进程不同,但它们的地址空间是相同的。此时,父进程和子进程会从fork调用的下一行代码开始执行。

exec的工作原理

exec系列函数用于替换当前进程的映像。调用exec后,当前进程的代码、数据和堆栈都会被新程序替换。exec不会返回,除非发生错误。

示例:使用fork和exec

以下是一个示例,展示如何使用forkexec创建子进程并执行新的程序。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    pid_t pid = fork(); // 创建新进程

    if (pid < 0) {
        // fork失败
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process (PID: %d) is executing 'ls' command.\n", getpid());
        // 使用exec执行ls命令
        execlp("ls", "ls", "-l", NULL);
        // 如果exec失败
        perror("exec failed");
        _exit(1);
    } else {
        // 父进程
        printf("Parent process (PID: %d) is waiting for child process to finish.\n", getpid());
        wait(NULL); // 等待子进程结束
        printf("Child process has finished.\n");
    }

    return 0;
}

代码总结

在上述代码中,父进程使用fork创建了一个子进程。子进程调用execlp执行ls -l命令,替换自身的映像。父进程等待子进程结束并输出相关信息。

结果说明

运行该程序后,您将看到子进程执行ls -l命令的输出,随后父进程输出子进程结束的信息。

3.3 进程间通信机制

在多进程环境中,进程间通信(IPC)是一个重要的主题。UNIX提供了多种IPC机制,包括管道、消息队列、共享内存和信号等。

管道

管道是一种常用的IPC机制,允许一个进程将数据写入管道,另一个进程从管道中读取数据。管道是单向的,通常用于父子进程之间的通信。

示例:使用管道进行进程间通信

以下是一个示例,展示如何使用管道在父进程和子进程之间传递数据。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    int pipefd[2];
    char buffer[20];

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("Pipe failed");
        return 1;
    }

    pid_t pid = fork(); // 创建新进程

    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        close(pipefd[1]); // 关闭写端
        read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据
        printf("Child process received: %s\n", buffer);
        close(pipefd[0]); // 关闭读端
    } else {
        // 父进程
        close(pipefd[0]); // 关闭读端
        const char *message = "Hello from parent!";
        write(pipefd[1], message, sizeof(message)); // 向管道写入数据
        close(pipefd[1]); // 关闭写端
        wait(NULL); // 等待子进程结束
    }

    return 0;
}

代码总结

在上述代码中,我们使用pipe创建了一个管道。父进程向管道写入数据,子进程从管道读取数据并输出。注意在使用管道时,父进程和子进程需要关闭不使用的管道端。

结果说明

运行该程序后,您将看到子进程输出从父进程接收到的消息,表明管道通信成功。

小结

本章介绍了进程管理的基本概念,包括进程的创建与终止、forkexec的使用,以及进程间通信机制。通过理解这些内容,读者将能够更有效地进行系统级编程,并为后续章节的学习打下坚实的基础。接下来,我们将深入探讨文件系统的相关内容。

第四章 文件系统

4.1 文件系统的基本概念

文件系统是操作系统中用于管理文件和目录的组件。它提供了文件的创建、删除、读写和权限管理等功能。UNIX文件系统的设计理念是将一切视为文件,包括设备和进程,从而提供统一的接口。

文件系统的结构

UNIX文件系统通常采用层次结构,根目录(/)是所有文件和目录的起点。文件系统中的每个文件和目录都有一个唯一的路径,路径可以是绝对路径或相对路径。

  • 绝对路径:从根目录开始的完整路径,例如/home/user/file.txt
  • 相对路径:相对于当前工作目录的路径,例如../file.txt

4.2 文件操作API详解

UNIX提供了一系列系统调用用于文件操作,包括打开、关闭、读写文件等。以下是一些常用的文件操作API:

  • open:打开文件并返回文件描述符。
  • close:关闭文件描述符。
  • read:从文件中读取数据。
  • write:向文件中写入数据。
  • lseek:移动文件指针。

示例:文件的打开、读写与关闭

以下是一个简单的C语言示例,展示如何使用文件操作API创建、写入和读取文件。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *filename = "example.txt";
    const char *text = "Hello, UNIX File System!\n";
    
    // 打开文件以写入
    int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("Error opening file for writing");
        return 1;
    }

    // 写入内容到文件
    ssize_t bytes_written = write(fd, text, strlen(text));
    if (bytes_written == -1) {
        perror("Error writing to file");
        close(fd);
        return 1;
    }

    // 关闭文件描述符
    close(fd);
    printf("File written successfully.\n");

    // 重新打开文件以读取
    fd = open(filename, O_RDONLY);
    if (fd == -1) {
        perror("Error opening file for reading");
        return 1;
    }

    // 读取文件内容
    char buffer[100];
    ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
    if (bytes_read == -1) {
        perror("Error reading from file");
        close(fd);
        return 1;
    }

    // 添加字符串结束符
    buffer[bytes_read] = '\0';
    printf("File content: %s", buffer);

    // 关闭文件描述符
    close(fd);
    return 0;
}

代码总结

在上述代码中,我们首先使用open系统调用以写入模式打开文件example.txt,并使用write将字符串写入文件。然后关闭文件描述符,重新以读取模式打开文件,使用read读取文件内容并输出。

结果说明

运行该程序后,您将看到文件example.txt被成功创建并写入内容,随后输出文件的内容,表明读写操作成功。

4.3 目录操作与文件权限管理

UNIX文件系统中的目录也是一种特殊的文件,目录用于组织文件。常用的目录操作包括创建目录、删除目录和列出目录内容。

目录操作API

  • mkdir:创建新目录。
  • rmdir:删除空目录。
  • opendirreaddirclosedir:打开、读取和关闭目录。

示例:创建和列出目录

以下是一个示例,展示如何创建目录并列出目录中的文件。

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    const char *dir_name = "example_dir";

    // 创建目录
    if (mkdir(dir_name, 0755) == -1) {
        perror("Error creating directory");
        return 1;
    }
    printf("Directory created successfully: %s\n", dir_name);

    // 打开目录
    DIR *dir = opendir(dir_name);
    if (dir == NULL) {
        perror("Error opening directory");
        return 1;
    }

    // 读取目录内容
    struct dirent *entry;
    printf("Contents of directory '%s':\n", dir_name);
    while ((entry = readdir(dir)) != NULL) {
        printf("%s\n", entry->d_name);
    }

    // 关闭目录
    closedir(dir);
    return 0;
}

代码总结

在上述代码中,我们使用mkdir创建了一个名为example_dir的目录。接着,使用opendir打开该目录,并使用readdir读取目录中的文件名,最后关闭目录。

结果说明

运行该程序后,您将看到新创建的目录,并列出该目录中的内容(可能为空,因为没有文件被添加)。

4.4 文件系统的性能优化

在UNIX文件系统中,性能优化是一个重要的主题。以下是一些常见的优化策略:

  • 使用缓冲区:通过使用缓冲区减少磁盘I/O操作的次数。
  • 异步I/O:允许程序在等待I/O操作完成时继续执行其他任务。
  • 合理的文件权限管理:确保文件权限设置合理,避免不必要的权限检查。

示例:使用缓冲区进行文件写入

以下是一个示例,展示如何使用标准I/O库的缓冲区进行文件写入。

#include <stdio.h>

int main() {
    const char *filename = "buffered_example.txt";
    FILE *file = fopen(filename, "w");
    if (file == NULL) {
        perror("Error opening file for writing");
        return 1;
    }

    // 使用缓冲区写入数据
    const char *text = "Hello, Buffered File System!\n";
    fprintf(file, "%s", text);

    // 关闭文件
    fclose(file);
    printf("Buffered file written successfully.\n");
    return 0;
}

代码总结

在上述代码中,我们使用标准I/O库的fopen打开文件,并使用fprintf将数据写入文件。标准I/O库会自动处理缓冲区,提高写入性能。

结果说明

运行该程序后,您将看到文件buffered_example.txt被成功创建并写入内容,表明缓冲区写入操作成功。

小结

本章介绍了UNIX文件系统的基本概念,包括文件操作API、目录操作与文件权限管理,以及文件系统的性能优化。通过理解这些内容,读者将能够更有效地进行文件操作,并为后续章节的学习打下坚实的基础。接下来,我们将深入探讨I/O操作的相关内容。

第五章 I/O操作

5.1 I/O操作的基本概念

I/O(输入/输出)操作是计算机系统中与外部设备进行数据交换的过程。在UNIX系统中,I/O操作通常分为两类:低级I/O和高级I/O。低级I/O直接与文件描述符交互,而高级I/O则通过标准I/O库提供更为简便的接口。

低级I/O与高级I/O的区别

  • 低级I/O:直接使用系统调用,如readwriteopenclose,提供对文件描述符的直接操作。
  • 高级I/O:使用标准I/O库(如stdio.h)中的函数,如fopenfreadfwritefclose,提供更高层次的抽象,通常更易于使用。

5.2 低级I/O与高级I/O的区别

低级I/O和高级I/O各有优缺点,适用于不同的场景。低级I/O提供更高的灵活性和控制,但编程复杂度较高;而高级I/O则简化了编程过程,但可能在性能上有所折扣。

低级I/O示例

以下是一个使用低级I/O进行文件读写的示例:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    const char *filename = "low_level_example.txt";
    const char *text = "Hello, Low-Level I/O!\n";

    // 使用低级I/O打开文件
    int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("Error opening file for writing");
        return 1;
    }

    // 写入内容到文件
    ssize_t bytes_written = write(fd, text, strlen(text));
    if (bytes_written == -1) {
        perror("Error writing to file");
        close(fd);
        return 1;
    }

    // 关闭文件描述符
    close(fd);
    printf("Low-level file written successfully.\n");
    return 0;
}

高级I/O示例

以下是一个使用高级I/O进行文件读写的示例:

#include <stdio.h>

int main() {
    const char *filename = "high_level_example.txt";
    const char *text = "Hello, High-Level I/O!\n";

    // 使用高级I/O打开文件
    FILE *file = fopen(filename, "w");
    if (file == NULL) {
        perror("Error opening file for writing");
        return 1;
    }

    // 写入内容到文件
    fprintf(file, "%s", text);

    // 关闭文件
    fclose(file);
    printf("High-level file written successfully.\n");
    return 0;
}

代码总结

在低级I/O示例中,我们使用openwrite系统调用直接操作文件描述符。而在高级I/O示例中,我们使用fopenfprintf函数进行文件操作。两者都能成功写入文件,但高级I/O提供了更简洁的接口。

结果说明

运行这两个程序后,您将看到分别创建了low_level_example.txthigh_level_example.txt文件,文件内容分别为“Hello, Low-Level I/O!”和“Hello, High-Level I/O!”。

5.3 标准I/O流的使用

UNIX系统中的标准I/O流包括标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。这些流提供了与用户交互的基本方式。

标准输入输出示例

以下是一个简单的示例,展示如何使用标准I/O流进行输入和输出。

#include <stdio.h>

int main() {
    char name[50];

    // 从标准输入读取用户输入
    printf("Enter your name: ");
    fgets(name, sizeof(name), stdin);

    // 输出到标准输出
    printf("Hello, %s", name);
    return 0;
}

代码总结

在上述代码中,我们使用fgets从标准输入读取用户的名字,并使用printf将其输出到标准输出。

结果说明

运行该程序后,用户可以输入自己的名字,程序将输出“Hello, [用户输入的名字]”。

5.4 异步I/O与缓冲技术

异步I/O是一种允许程序在等待I/O操作完成时继续执行其他任务的技术。它可以提高程序的效率,特别是在处理大量I/O操作时。

异步I/O示例

在UNIX中,异步I/O通常通过aio库实现。以下是一个简单的异步I/O示例:

#include <stdio.h>
#include <aio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const char *filename = "async_example.txt";
    const char *text = "Hello, Asynchronous I/O!\n";
    struct aiocb cb;

    // 初始化aiocb结构
    memset(&cb, 0, sizeof(struct aiocb));
    cb.aio_fildes = open(filename, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
    cb.aio_buf = (void *)text;
    cb.aio_nbytes = strlen(text);

    // 异步写入文件
    if (aio_write(&cb) == -1) {
        perror("Error initiating async write");
        return 1;
    }

    // 等待写入完成
    while (aio_error(&cb) == EINPROGRESS) {
        // 可以在这里执行其他任务
    }

    // 检查写入结果
    if (aio_return(&cb) == -1) {
        perror("Error during async write");
        return 1;
    }

    printf("Asynchronous write completed successfully.\n");
    close(cb.aio_fildes);
    return 0;
}

代码总结

在上述代码中,我们使用aio_write发起异步写入操作,并使用aio_error检查操作状态。程序可以在等待I/O完成时执行其他任务。

结果说明

运行该程序后,您将看到文件async_example.txt被成功创建并写入内容,表明异步I/O操作成功。

5.5 磁盘I/O的实现与优化

磁盘I/O是计算机系统中最常见的I/O操作之一。优化磁盘I/O可以显著提高程序性能。以下是一些常见的优化策略:

  • 使用缓冲区:通过使用缓冲区减少磁盘I/O操作的次数。
  • 合并I/O操作:将多个小的I/O操作合并为一个大的I/O操作,减少系统调用的开销。
  • 异步I/O:允许程序在等待I/O操作完成时继续执行其他任务。

示例:使用缓冲区进行磁盘I/O

以下是一个示例,展示如何使用缓冲区进行磁盘I/O操作。

#include <stdio.h>
#include <stdlib.h>

int main() {
    const char *filename = "buffered_disk_io.txt";
    FILE *file = fopen(filename, "w");
    if (file == NULL) {
        perror("Error opening file for writing");
        return 1;
    }

    // 使用缓冲区写入数据
    for (int i = 0; i < 100; i++) {
        fprintf(file, "Line %d: Hello, Buffered Disk I/O!\n", i);
    }

    // 关闭文件
    fclose(file);
    printf("Buffered disk I/O completed successfully.\n");
    return 0;
}

代码总结

在上述代码中,我们使用标准I/O库的fopenfprintf函数进行磁盘I/O操作。通过使用缓冲区,我们可以有效地减少磁盘I/O操作的次数。

结果说明

运行该程序后,您将看到文件buffered_disk_io.txt被成功创建并写入100行内容,表明磁盘I/O操作成功。

小结

本章介绍了I/O操作的基本概念,包括低级I/O与高级I/O的区别、标准I/O流的使用、异步I/O与缓冲技术,以及磁盘I/O的实现与优化。通过理解这些内容,读者将能够更有效地进行I/O操作,并为后续章节的学习打下坚实的基础。接下来,我们将深入探讨信号处理的相关内容。

第六章 信号处理

6.1 信号的基本概念

在UNIX系统中,信号是一种用于通知进程发生特定事件的机制。信号可以由操作系统、用户或其他进程发送,通常用于进程间的通信和控制。信号的处理是编写可靠和响应迅速的程序的关键。

信号的类型

UNIX系统中有多种信号,常见的信号包括:

  • SIGINT:中断信号,通常由用户按下Ctrl+C发送。
  • SIGTERM:终止信号,请求进程终止。
  • SIGKILL:强制终止信号,无法被捕获或忽略。
  • SIGUSR1SIGUSR2:用户自定义信号,可以用于进程间通信。

6.2 常见信号类型与处理函数

每个信号都有一个默认的处理方式,进程可以选择捕获信号并定义自定义的处理函数。使用signalsigaction系统调用可以设置信号处理函数。

示例:捕获SIGINT信号

以下是一个简单的C语言示例,展示如何捕获SIGINT信号并定义自定义处理函数。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
    printf("Caught SIGINT (signal %d). Exiting gracefully...\n", sig);
    exit(0);
}

int main() {
    // 设置SIGINT信号的处理函数
    signal(SIGINT, handle_sigint);

    printf("Press Ctrl+C to trigger SIGINT signal...\n");

    // 无限循环,等待信号
    while (1) {
        sleep(1); // 暂停1秒
    }

    return 0;
}

代码总结

在上述代码中,我们定义了一个名为handle_sigint的信号处理函数,用于处理SIGINT信号。通过signal函数将SIGINT信号与处理函数关联。当用户按下Ctrl+C时,程序将捕获信号并执行自定义处理。

结果说明

运行该程序后,您将看到提示信息,程序将进入无限循环。当您按下Ctrl+C时,程序将捕获SIGINT信号并输出相应的消息,然后正常退出。

6.3 信号安全的编程实践

在编写信号处理程序时,需要遵循一些安全的编程实践,以确保程序的可靠性和稳定性。以下是一些建议:

  • 避免使用不安全的函数:在信号处理函数中,避免调用不安全的函数(如printfmalloc等),因为这些函数在信号处理过程中可能导致不确定的行为。
  • 使用原子操作:确保信号处理函数中的操作是原子性的,以避免数据竞争和不一致性。
  • 设置信号屏蔽:在信号处理函数中,可以使用sigprocmask设置信号屏蔽,以防止信号在处理过程中再次被触发。

示例:使用sigaction设置信号处理

以下是一个示例,展示如何使用sigaction设置信号处理函数,并确保信号安全。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_sigint(int sig) {
    write(STDOUT_FILENO, "Caught SIGINT. Exiting gracefully...\n", 37);
    exit(0);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handle_sigint; // 设置处理函数
    sigemptyset(&sa.sa_mask); // 清空信号屏蔽
    sa.sa_flags = 0; // 默认标志

    // 设置SIGINT信号的处理函数
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction failed");
        return 1;
    }

    printf("Press Ctrl+C to trigger SIGINT signal...\n");

    // 无限循环,等待信号
    while (1) {
        sleep(1); // 暂停1秒
    }

    return 0;
}

代码总结

在上述代码中,我们使用sigaction设置了SIGINT信号的处理函数。通过sigemptyset清空信号屏蔽,确保信号处理的安全性。

结果说明

运行该程序后,您将看到提示信息,程序将进入无限循环。当您按下Ctrl+C时,程序将捕获SIGINT信号并输出相应的消息,然后正常退出。

6.4 信号处理在进程协调中的应用

信号处理在进程协调中起着重要作用,尤其是在多进程程序中。通过信号,进程可以相互通信,协调工作。

示例:使用信号实现进程间通信

以下是一个示例,展示如何使用信号实现父子进程间的通信。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void handle_sigusr1(int sig) {
    printf("Child process received SIGUSR1 signal.\n");
}

int main() {
    pid_t pid = fork(); // 创建新进程

    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        signal(SIGUSR1, handle_sigusr1); // 设置SIGUSR1信号的处理函数
        printf("Child process waiting for SIGUSR1 signal...\n");
        pause(); // 等待信号
        printf("Child process exiting.\n");
        exit(0);
    } else {
        // 父进程
        sleep(2); // 等待子进程准备好
        printf("Parent process sending SIGUSR1 signal to child process.\n");
        kill(pid, SIGUSR1); // 发送SIGUSR1信号
        wait(NULL); // 等待子进程结束
    }

    return 0;
}

代码总结

在上述代码中,父进程创建了一个子进程,并设置了SIGUSR1信号的处理函数。父进程等待2秒后,向子进程发送SIGUSR1信号,子进程捕获信号并执行相应的处理。

结果说明

运行该程序后,您将看到父进程发送信号的消息,子进程接收到信号并输出相应的消息,然后正常退出。

小结

本章介绍了信号处理的基本概念,包括信号的类型、信号处理函数的设置、信号安全的编程实践,以及信号在进程协调中的应用。通过理解这些内容,读者将能够编写更可靠和响应迅速的程序,并为后续章节的学习打下坚实的基础。接下来,我们将深入探讨网络编程的相关内容。

第七章 网络编程

7.1 网络编程基础知识

网络编程是指在计算机网络中进行数据传输和通信的编程技术。UNIX和Linux系统提供了丰富的网络编程接口,主要通过套接字(socket)实现。套接字是一种用于进程间通信的抽象,支持不同主机之间的网络通信。

套接字的类型

在网络编程中,主要有两种类型的套接字:

  • 流套接字(SOCK_STREAM):用于TCP协议,提供可靠的、面向连接的通信。
  • 数据报套接字(SOCK_DGRAM):用于UDP协议,提供无连接的、不可靠的通信。

7.2 套接字API的使用

UNIX系统提供了一系列API用于创建和操作套接字。以下是一些常用的套接字API:

  • socket:创建一个新的套接字。
  • bind:将套接字与本地地址和端口绑定。
  • listen:监听连接请求。
  • accept:接受连接请求。
  • connect:连接到远程套接字。
  • sendrecv:发送和接收数据。

示例:TCP服务器和客户端

以下是一个简单的TCP服务器和客户端示例,展示如何使用套接字进行网络通信。

TCP服务器示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定套接字
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("Set socket options failed");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    // 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("Accept failed");
        exit(EXIT_FAILURE);
    }

    // 读取数据
    read(new_socket, buffer, BUFFER_SIZE);
    printf("Message from client: %s\n", buffer);

    // 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

代码总结

在上述TCP服务器示例中,我们创建了一个套接字并将其绑定到指定的端口。服务器开始监听连接请求,并在接受连接后读取客户端发送的数据。

结果说明

运行该程序后,服务器将开始监听端口8080,等待客户端连接。

TCP客户端示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *message = "Hello from client!";

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将IPv4地址从文本转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }

    // 发送数据
    send(sock, message, strlen(message), 0);
    printf("Message sent from client: %s\n", message);

    // 关闭套接字
    close(sock);
    return 0;
}

代码总结

在上述TCP客户端示例中,我们创建了一个套接字并连接到服务器。客户端发送一条消息到服务器,然后关闭套接字。

结果说明

运行客户端程序后,您将看到客户端成功连接到服务器并发送消息,服务器将输出接收到的消息。

7.3 TCP/IP协议栈的原理

TCP/IP协议栈是网络通信的基础,包含多个层次的协议。主要分为四个层次:

  1. 应用层:提供网络服务的应用程序,如HTTP、FTP等。
  2. 传输层:负责数据的传输和完整性,主要协议有TCP和UDP。
  3. 网络层:负责数据包的路由和转发,主要协议有IP。
  4. 链路层:负责物理网络的传输,处理硬件相关的细节。

TCP与UDP的区别

  • TCP:面向连接,提供可靠的数据传输,适用于需要保证数据完整性的应用(如HTTP、FTP)。
  • UDP:无连接,不保证数据的可靠性,适用于对速度要求高但对数据完整性要求低的应用(如视频流、在线游戏)。

7.4 网络应用开发的最佳实践

在进行网络编程时,遵循一些最佳实践可以提高程序的可靠性和性能:

  • 错误处理:始终检查系统调用的返回值,并处理可能的错误。
  • 资源管理:确保在程序结束时关闭所有打开的套接字,避免资源泄漏。
  • 并发处理:使用多线程或多进程处理多个客户端连接,提高服务器的并发能力。
  • 数据加密:在传输敏感数据时,使用SSL/TLS等加密技术保护数据安全。

示例:使用多线程处理客户端连接

以下是一个示例,展示如何使用多线程处理多个客户端连接。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT 8080
#define BUFFER_SIZE 1024

void *handle_client(void *socket_desc) {
    int sock = *(int *)socket_desc;
    char buffer[BUFFER_SIZE] = {0};

    // 读取数据
    read(sock, buffer, BUFFER_SIZE);
    printf("Message from client: %s\n", buffer);

    // 关闭套接字
    close(sock);
    return NULL;
}

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket failed");
        exit(EXIT_FAILURE);
    }

    // 绑定套接字
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("Set socket options failed");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("Listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d...\n", PORT);

    // 接受连接
    while ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) >= 0) {
        pthread_t thread_id;
        // 创建新线程处理客户端
        if (pthread_create(&thread_id, NULL, handle_client, (void *)&new_socket) != 0) {
            perror("Could not create thread");
            return 1;
        }
        pthread_detach(thread_id); // 使线程在结束后自动回收
    }

    // 关闭服务器套接字
    close(server_fd);
    return 0;
}

代码总结

在上述多线程TCP服务器示例中,我们使用pthread_create创建新线程处理每个客户端连接。每个线程读取客户端发送的数据并输出。

结果说明

运行该程序后,服务器将能够同时处理多个客户端连接,提高并发能力。

小结

本章介绍了网络编程的基础知识,包括套接字的使用、TCP/IP协议栈的原理以及网络应用开发的最佳实践。通过理解这些内容,读者将能够编写高效的网络应用程序,并为后续章节的学习打下坚实的基础。


本文内容来源于精品资源《UNIX环境高级编程(中文版)》,点击前往

简介:
《UNIX环境高级编程》是一本深受程序员和系统管理员喜爱的经典之作,它由W. Richard Stevens撰写,对中国乃至全球的UNIX和Linux开发者具有深远影响。这本书深入浅出地讲解了在UNIX系统上进行程序开发的各种技术和实践,对于理解操作系统内核与用户空间之间的交互,以及如何高效、稳定地编写系统级程序至关重要。

本书涵盖了UNIX系统的基本概念,包括进程管理、文件系统、I/O操作、信号处理等核心主题。通过学习,读者可以掌握如何创建和管理进程,了解进程间通信机制如管道、套接字和共享内存,以及如何利用fork和exec函数家族来实现进程的创建和替换。

书中详尽解析了文件系统的工作原理和编程接口,包括打开、关闭、读写文件的API,以及目录操作和文件权限管理。这对于编写涉及文件操作的任何程序都是必不可少的知识。

再者,I/O操作是UNIX编程的重要组成部分。书中详细阐述了低级I/O(如read和write)与高级I/O(如stdio库)的区别,以及异步I/O和缓冲技术的应用。同时,还介绍了标准I/O流和磁盘I/O的实现方式,帮助开发者理解和优化程序性能。

信号处理是UNIX环境中用于进程协调的关键机制。书中的这一部分介绍了各种信号类型、信号处理函数以及如何编写信号安全的代码,这对于编写可靠和响应迅速的程序至关重要。

此外,书中还涵盖了网络编程的基础知识,如套接字API的使用,以及TCP/IP协议栈的原理。这部分内容对现代互联网应用的开发者尤其重要,因为它提供了在网络环境下进行通信的工具和方法。

书中还探讨了诸如进程环境、进程组和会话、终端管理、标准I/O重定向等高级话题,这些内容对于编写脚本和管理系统服务非常实用。

《UNIX环境高级编程》是一部全面且深入的教程,不仅适合初学者建立坚实的UNIX编程基础,也对有经验的开发者提供了一种深入理解系统内部运作的宝贵资源。无论你是想提升系统编程技能,还是解决实际工作中的难题,这本书都将是你不可或缺的参考书籍。通过阅读和实践书中的示例,你将能够更熟练地驾驭UNIX和Linux环境,编写出高效、可靠的软件。


  • 24
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
本书全面介绍了UNIX系统的程序设计界面—系统调用界面和标准C库提供的许多函数。 本书的前15章着重于理论知识的阐述,主要内容包括UNIX文件和目录、进程环境、进程控制、 进程间通信以及各种I/O。在此基础上,分别按章介绍了多个应用实例,包括如何创建数据库函数库, PostScript 打印机驱动程序,调制解调器拨号器及在伪终端上运行其他程序的程序等。 本书内容丰富权威, 概念清晰精辟,一直以来被誉为UNIX编程的“圣经”,对于所有UNIX程序员—无论是初学者还是专家级人士 —都是一本无价的参考书籍。 目 录 译者序 译者简介 前言 第1章 UNIX基础知识 1 1.1 引言 1 1.2 登录 1 1.2.1 登录名 1 1.2.2 shell 1 1.3 文件和目录 2 1.3.1 文件系统 2 1.3.2 文件名 2 1.3.3 路径名 2 1.3.4 工作目录 4 1.3.5 起始目录 4 1.4 输入和输出 5 1.4.1 文件描述符 5 1.4.2 标准输入、标准输出和标准 出错 5 1.4.3 不用缓存的I/O 5 1.4.4 标准I/O 6 1.5 程序和进程 7 1.5.1 程序 7 1.5.2 进程和进程ID 7 1.5.3 进程控制 7 1.6 ANSI C 9 1.6.1 函数原型 9 1.6.2 类属指针 9 1.6.3 原始系统数据类型 10 1.7 出错处理 10 1.8 用户标识 11 1.8.1 用户ID 11 1.8.2 组ID 12 1.8.3 添加组ID 12 1.9 信号 12 1.10 UNIX时间值 14 1.11 系统调用和库函数 14 1.12 小结 16 习题 16 第2章 UNIX标准化及实现 17 2.1 引言 17 2.2 UNIX标准化 17 2.2.1 ANSI C 17 2.2.2 IEEE POSIX 18 2.2.3 X/Open XPG3 19 2.2.4 FIPS 19 2.3 UNIX实现 19 2.3.1 SVR4 20 2.3.2 4.3+BSD 20 2.4 标准和实现的关系 21 2.5 限制 21 2.5.1 ANSI C限制 22 2.5.2 POSIX限制 22 2.5.3 XPG3限制 24 2.5.4 sysconf、pathconf 和fpathconf 函数 24 2.5.5 FIPS 151-1要求 28 2.5.6 限制总结 28 2.5.7 未确定的运行时间限制 29 2.6 功能测试宏 32 2.7 基本系统数据类型 32 2.8 标准之间的冲突 33 2.9 小结 34 习题 34 第3章 文件I/O 35 3.1 引言 35 3.2 文件描述符 35 3.3 open函数 35 3.4 creat函数 37 3.5 close函数 37 3.6 lseek函数 38 3.7 read函数 40 3.8 write函数 41 3.9 I/O的效率 41 3.10 文件共享 42 3.11 原子操作 45 3.11.1 添加至一个文件 45 3.11.2 创建一个文件 45 3.12 dup和dup2函数 46 3.13 fcntl函数 47 3.14 ioctl函数 50 3.15 /dev/fd 51 3.16 小结 52 习题 52 第4章 文件和目录 54 4.1 引言 54 4.2 stat, fstat和lstat函数 54 4.3 文件类型 55 4.4 设置-用户-ID和设置-组-ID 57 4.5 文件存取许可权 58 4.6 新文件和目录的所有权 60 4.7 access函数 60 4.8 umask函数 62 4.9 chmod和fchmod函数 63 4.10 粘住位 65 4.11 chown, fchown和 lchown函数 66 4.12 文件长度 67 4.13 文件截短 68 4.14 文件系统 69 4.15 link, unlink, remove和rename 函数 71 4.16 符号连接 73 4.17 symlink 和readlink函数 76 4.18 文件的时间 76 4.19 utime函数 78 4.20 mkdir和rmdir函数
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdn_te_DOWNload_005

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值