53、Linux标准与系统结构详解

Linux标准与系统结构详解

1. Linux初始化脚本标准

在Linux系统中,LSB 3.1规定初始化脚本的位置为 /etc/init.d ,不过也允许将其设置为指向其他位置的链接。 /etc/init.d 中的每个脚本都有一个与它所提供的服务相关的名称。由于这是所有Linux服务共享的公共命名空间,因此名称的唯一性非常重要。例如,如果MySQL和PostgreSQL都将它们的脚本命名为“database”,就会引发冲突。为避免这种情况,有另一个标准组织——The Linux Assigned Names And Numbers Authority(LANANA),其官网为http://www.lanana.org/ 。它维护着一个脚本和软件包的注册名称列表,为Linux系统的用户提供了便利。

初始化脚本必须接受一个参数来控制其操作,定义的参数及其含义如下表所示:
| 参数 | 含义 |
| — | — |
| start | 启动(或重启)服务 |
| stop | 停止服务 |
| restart | 重启服务,通常先停止服务,然后再启动 |
| reload | 重置服务,重新加载参数,但不停止服务。并非所有服务都支持此选项,所以有些脚本可能不接受该参数,或者接受但无实际效果 |
| force - reload | 如果服务支持,则尝试重新加载;否则,重启服务 |
| status | 打印服务状态的文本消息,并返回一个状态码,用于确定服务的状态 |

所有命令执行成功时返回0,失败时返回一个表示失败原因的错误代码。对于 status 命令,如果服务正在运行则返回0,其他代码表示服务因某种原因未运行。

2. 文件系统层次结构标准(FHS)

FHS(Filesystem Hierarchy Standard)的目的是定义Linux文件系统中的标准位置,让开发者和用户能够合理预期在哪里找到所需的内容。长期使用类UNIX操作系统的用户一直抱怨不同系统文件系统布局的细微差异,而FHS为Linux发行版提供了一种避免碎片化的方式。其官网为http://www.pathname.com/fhs/ 。

Linux系统中的文件布局最初看似是基于历史实践的半随意排列,但随着时间的推移,它已经演变成了我们现在看到的层次结构。总体思路是将文件和目录分为三组:
- 特定Linux系统独有的文件和目录,如启动脚本和配置文件。
- 只读且可在不同Linux系统之间共享的文件和目录,如应用程序可执行文件。
- 可读写且可在Linux系统或其他操作系统之间共享的目录,如用户主目录。

FHS定义的顶级结构包含几个必需的子目录和少量可选目录,主要的目录及其用途如下表所示:
| 目录 | 是否必需 | 用途 |
| — | — | — |
| /bin | 是 | 重要的系统二进制文件 |
| /boot | 是 | 系统启动所需的文件 |
| /dev | 是 | 设备文件 |
| /etc | 是 | 系统配置文件 |
| /home | 否 | 用户文件目录 |
| /lib | 是 | 标准库 |
| /media | 是 | 可移动媒体的挂载点,为系统支持的每种媒体类型提供单独的子目录 |
| /mnt | 是 | 临时挂载设备(如CD - ROM和闪存盘)的便捷位置 |
| /opt | 是 | 额外的应用程序软件 |
| /root | 否 | 根用户的文件 |
| /sbin | 是 | 系统启动期间必需的重要系统二进制文件 |
| /srv | 是 | 此系统提供的服务的只读数据 |
| /tmp | 是 | 临时文件 |
| /usr | 是 | 二级层次结构。传统上也存储用户文件,但现在不建议普通用户对其进行写操作 |
| /var | 是 | 可变数据,如日志文件 |

此外,还可能存在以 lib 开头的其他目录,但这种情况并不常见。通常还会看到 /lost+found 目录(用于文件系统恢复)和 /proc 目录,它是一个伪文件系统,提供对当前运行系统的映射。当前版本的FHS标准强烈建议存在 /proc 文件系统,但并非必需。

下面简要介绍根目录下各标准子目录的用途:
- /bin :包含根用户和普通用户都可使用的二进制文件,在单用户模式下,当其他一些目录结构可能未挂载时,这些文件对系统运行至关重要。例如, cat ls sh 等核心命令通常位于此目录。
- /boot :用于存放Linux系统启动所需的文件。该目录通常较小,小于100 MB,并且常作为一个单独的分区。在基于PC的系统中,BIOS对活动分区有一定限制,要求其位于磁盘的前2G或4G内,将 /boot 作为单独分区可以在规划磁盘分区时提供更多灵活性。
- /dev :包含映射到硬件的特殊设备文件。例如, /dev/hda 将映射到第一个IDE磁盘。
- /etc :包含系统配置文件。过去,这里也可能有一些二进制文件,但在大多数Linux系统中已不再如此。 /etc 目录中最著名的文件可能是 passwd ,它包含用户信息。其他有用的文件包括 fstab (列出挂载选项)、 hosts (列出IP到主机名的映射)以及 httpd 目录(包含Apache服务器的配置)。
- /home :用于存放用户文件。通常,每个用户在该目录下都有一个与他们登录名相同的目录,这将是他们的默认登录目录。例如,用户 rick 登录后,很可能会进入 /home/rick 目录。
- /lib :包含必需的共享库和内核模块,特别是系统启动或单用户模式下所需的那些。
- /media :作为顶级目录,用于包含其他可移动媒体的挂载点目录。其目的是减少不必要的顶级目录,如 /cdrom /floppy
- /mnt :只是一个临时挂载额外文件系统的便捷位置。过去,一些发行版在 /mnt 下为不同设备添加了子目录,如 /cdrom /floppy ,但现在建议将这些目录放在 /media 下,使 /mnt 恢复其作为单一顶级临时挂载位置的原始用途。
- /opt :供软件供应商在基础发行版中添加额外软件应用程序时使用。发行版不应将其用于标准发行版中的软件,而应留给第三方供应商使用。通常,供应商会创建一个以其名称命名的子目录,然后再创建 /bin /lib 等子目录,用于存放特定于其应用程序的文件。按照惯例,许多开源Linux软件包使用 /usr/local 进行安装。
- /root :用于存放根用户的文件。它不在 /home 目录树中,因为在单用户模式下, /home 可能未挂载。
- /sbin :用于存放通常仅由系统管理员使用的命令,以及系统启动或单用户模式下必需的命令,如 fsck halt swapon
- /srv :用于存放特定站点的只读配置数据,但目前并不常用。
- /tmp :用于存放临时文件。系统启动时,该目录通常(但不总是)会被清空。
- /usr :是一个较为复杂的二级文件系统,通常包含系统启动或单用户模式下不需要的所有系统命令和库。它有许多子目录,如 /bin /lib /X11R6 /local 。早期的UNIX和Linux系统中, /usr 还包含日志、邮件假脱机等子目录,现在这些都已移到 /var 目录。这样做的好处是, /usr 现在可以作为一个可挂载的文件系统,并且大多数时候可以以只读方式挂载。当 /usr 以只读方式挂载时,它可以通过网络共享给其他系统,并且在系统因电源故障等原因意外停止时,不太容易损坏。
- /var :包含经常变化的数据,如打印假脱机文件、应用程序日志文件和邮件假脱机目录。

下面是一个简单的mermaid流程图,展示了FHS目录结构的主要部分:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A[/]:::process --> B[/bin]:::process
    A --> C[/boot]:::process
    A --> D[/dev]:::process
    A --> E[/etc]:::process
    A --> F[/home]:::process
    A --> G[/lib]:::process
    A --> H[/media]:::process
    A --> I[/mnt]:::process
    A --> J[/opt]:::process
    A --> K[/root]:::process
    A --> L[/sbin]:::process
    A --> M[/srv]:::process
    A --> N[/tmp]:::process
    A --> O[/usr]:::process
    A --> P[/var]:::process
3. 编写和部署可移植Linux应用程序的其他考虑因素

如果要编写和部署完全可移植的Linux应用程序,还有许多其他因素需要考虑。

3.1 本地化

如果你想让应用程序支持不同的语言和区域设置,即使只使用英语,也会涉及货币、数字分隔符、日期格式等问题。有相关人员正在制定这些标准,你可以在http://www.openi18n.org/ 查看他们的工作。

3.2 目标系统的安装情况

另一个需要考虑的问题是目标系统安装了哪些选项、库版本等。幸运的是,由于前面提到的标准化工作,这个问题的严重性正在降低,但仍然可能是一个难题。有一对GNU工具 autoconf automake 可以在很大程度上帮助解决这个问题。虽然你可能没有直接使用过它们,但在从源代码安装软件时,输入 ./configure; make ,你几乎肯定已经受益于它们了。你可以在GNU网页http://www.gnu.org/software/autoconf/ 和http://www.gnu.org/software/automake 上找到更多关于它们的信息。

遵循这些标准有助于使Linux成为一个更易于编程的平台,并确保不同的Linux发行版符合一些基本标准,让程序员和用户的生活更加轻松。我们鼓励大家使用这些标准,并鼓励他人也这样做。

Linux标准与系统结构详解(续)

4. 系统初始化与脚本参数

在Linux系统中,系统初始化脚本起着至关重要的作用。正如前面提到的,LSB 3.1规定初始化脚本位于 /etc/init.d ,且脚本名称需与服务相关且唯一。下面详细介绍初始化脚本参数的操作及相关注意事项。

当执行初始化脚本时,不同的参数会触发不同的操作,具体操作流程如下:
1. 启动服务(start) :该参数用于启动或重启服务。当执行 start 参数时,脚本会检查服务当前状态,如果服务未运行,则启动服务;如果服务已运行,则可能会先停止服务,然后再重新启动。例如,对于一个名为 my_service 的服务,启动命令为 /etc/init.d/my_service start
2. 停止服务(stop) :此参数用于停止正在运行的服务。执行 stop 参数时,脚本会向服务发送停止信号,让服务正常关闭。如 /etc/init.d/my_service stop
3. 重启服务(restart) :通常是先执行 stop 操作,再执行 start 操作。这样可以确保服务以全新的状态运行。命令为 /etc/init.d/my_service restart
4. 重新加载参数(reload) :尝试在不停止服务的情况下,重置服务并重新加载参数。但并非所有服务都支持此操作,若服务不支持,该参数可能无效。使用方式为 /etc/init.d/my_service reload
5. 强制重新加载(force - reload) :如果服务支持 reload 操作,则执行 reload ;若不支持,则执行 restart 。命令是 /etc/init.d/my_service force - reload
6. 查看服务状态(status) :打印服务状态的文本消息,并返回一个状态码。若返回0,表示服务正在运行;其他代码表示服务未运行。例如 /etc/init.d/my_service status

所有这些命令执行成功时返回0,失败时返回相应的错误代码,方便用户判断操作结果。

5. 文件系统层次结构的深入理解

FHS定义的文件系统层次结构为Linux系统的组织和管理提供了标准。下面进一步分析各子目录的特点和使用场景。

5.1 /usr目录

/usr 目录是一个复杂的二级文件系统,包含了大量系统启动或单用户模式下不需要的系统命令和库。它的子目录众多,如 /usr/bin 包含了许多用户级别的可执行文件, /usr/lib 存放了各种库文件。在早期, /usr 还用于存储用户文件,但现在这种做法已不被推荐,普通用户不应向 /usr 目录写入数据。

例如,当安装一个新的软件包时,很多时候软件的可执行文件会被安装到 /usr/bin /usr/local/bin ,库文件会被安装到 /usr/lib /usr/local/lib 。这样的布局使得系统的软件管理更加规范。

5.2 /var目录

/var 目录用于存放可变数据,如日志文件、邮件假脱机目录等。这些数据会随着系统的运行而不断变化。例如,系统日志文件通常存放在 /var/log 目录下,不同的服务会有各自的日志文件,如 /var/log/messages 记录了系统的一般消息, /var/log/secure 记录了安全相关的信息。

邮件假脱机目录 /var/spool/mail 用于存放用户的邮件,当有新邮件到达时,会被存储在该目录下对应的用户邮箱中。

下面是一个表格,总结了 /usr /var 目录的主要特点:
| 目录 | 特点 | 用途示例 |
| — | — | — |
| /usr | 二级层次结构,包含系统命令和库,一般只读 | 安装软件的可执行文件和库文件 |
| /var | 存放可变数据 | 日志文件、邮件假脱机目录 |

6. 内存管理与调试

在Linux系统中,内存管理是一个重要的方面。了解内存的分配、使用和调试方法,有助于提高程序的性能和稳定性。

6.1 内存分配

内存分配主要通过 malloc calloc realloc 等函数实现。 malloc 用于分配指定大小的内存块,例如:

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

int main() {
    int *ptr;
    ptr = (int *)malloc(10 * sizeof(int));
    if (ptr == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    // 使用分配的内存
    for (int i = 0; i < 10; i++) {
        ptr[i] = i;
    }
    // 释放内存
    free(ptr);
    return 0;
}

calloc 在分配内存的同时会将内存块初始化为0, realloc 用于调整已分配内存块的大小。

6.2 内存调试

内存调试是解决程序中内存问题的重要手段。常见的内存问题包括内存泄漏、野指针等。有一些工具可以帮助进行内存调试,如 valgrind ElectricFence

valgrind 是一个强大的内存调试工具,可以检测内存泄漏、越界访问等问题。使用 valgrind 调试程序的步骤如下:
1. 编译程序时确保使用了调试信息,例如 gcc -g -o my_program my_program.c
2. 使用 valgrind 运行程序,如 valgrind --leak-check=full ./my_program
3. valgrind 会输出详细的内存使用信息和可能存在的问题。

ElectricFence 则是一个简单的内存保护工具,它可以帮助检测内存越界访问。使用时,需要在编译程序时链接 ElectricFence 库,例如 gcc -o my_program my_program.c -lefence

7. 进程与线程管理

Linux系统中的进程和线程是实现多任务处理的关键。了解它们的创建、调度和同步机制,有助于编写高效的程序。

7.1 进程管理

进程是程序在操作系统中的一次执行实例。创建进程主要通过 fork exec 系列函数实现。 fork 用于创建一个新的子进程,子进程是父进程的副本。例如:

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

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        printf("进程创建失败\n");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("这是子进程\n");
    } else {
        // 父进程
        printf("这是父进程,子进程ID为 %d\n", pid);
    }
    return 0;
}

exec 系列函数用于替换当前进程的映像,执行一个新的程序。例如 execlp 函数:

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

int main() {
    execlp("ls", "ls", "-l", NULL);
    printf("如果执行到这里,说明execlp调用失败\n");
    return 1;
}

进程的调度由操作系统负责,根据进程的优先级和资源需求进行合理分配。用户可以使用 nice renice 命令调整进程的优先级。

7.2 线程管理

线程是进程内的一个执行单元,多个线程可以在同一个进程内并发执行。在Linux中,使用POSIX线程库(pthread)来创建和管理线程。

创建线程的基本步骤如下:
1. 包含 pthread.h 头文件。
2. 定义线程函数。
3. 使用 pthread_create 函数创建线程。

以下是一个简单的线程示例:

#include <stdio.h>
#include <pthread.h>

void *thread_function(void *arg) {
    printf("这是一个线程\n");
    return NULL;
}

int main() {
    pthread_t thread_id;
    int result = pthread_create(&thread_id, NULL, thread_function, NULL);
    if (result != 0) {
        printf("线程创建失败\n");
        return 1;
    }
    pthread_join(thread_id, NULL);
    printf("主线程结束\n");
    return 0;
}

线程的同步可以使用互斥锁(mutex)和信号量(semaphore)等机制,确保多个线程安全地访问共享资源。

8. 网络编程与套接字

网络编程是Linux系统中实现网络通信的重要手段。套接字(socket)是网络编程的基础,通过套接字可以实现不同主机之间的通信。

8.1 套接字的基本概念

套接字是一种网络通信的抽象接口,它具有不同的属性,包括域(domain)、类型(type)和协议(protocol)。常见的套接字域有 AF_INET (IPv4)和 AF_UNIX (本地套接字),类型有 SOCK_STREAM (面向连接的流式套接字)和 SOCK_DGRAM (无连接的数据报套接字)。

创建套接字的基本步骤如下:
1. 使用 socket 函数创建套接字。
2. 绑定套接字地址(可选)。
3. 监听连接(对于服务器端)或发起连接(对于客户端)。
4. 进行数据的发送和接收。

以下是一个简单的TCP服务器和客户端示例:

服务器端代码

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

#define PORT 8888

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    const char *hello = "Hello from server";

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

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        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");
        exit(EXIT_FAILURE);
    }
    // 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    // 读取客户端发送的数据
    read(new_socket, buffer, 1024);
    printf("客户端消息: %s\n", buffer);
    // 发送响应消息
    send(new_socket, hello, strlen(hello), 0);
    printf("消息已发送\n");
    // 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

客户端代码

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

#define PORT 8888

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[1024] = {0};

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");
        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) {
        perror("Invalid address/ Address not supported");
        return -1;
    }
    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("Connection Failed");
        return -1;
    }
    // 发送消息
    send(sock, hello, strlen(hello), 0);
    printf("消息已发送\n");
    // 读取服务器响应
    read(sock, buffer, 1024);
    printf("服务器响应: %s\n", buffer);
    // 关闭套接字
    close(sock);
    return 0;
}

通过以上代码,客户端和服务器可以进行简单的TCP通信。

9. 总结

Linux系统拥有丰富的标准和机制,从系统初始化脚本到文件系统层次结构,从内存管理到网络编程,每个方面都有其独特的特点和使用方法。遵循这些标准和机制,有助于提高系统的可维护性、可移植性和性能。无论是开发者还是系统管理员,都应该深入了解这些知识,以便更好地使用和管理Linux系统。同时,不断学习和掌握新的技术和方法,能够让我们在Linux的世界中更加得心应手。

下面是一个mermaid流程图,展示了网络编程的基本流程:

graph LR
    classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
    A[创建套接字]:::process --> B[绑定地址(服务器端)]:::process
    B --> C[监听连接(服务器端)]:::process
    C --> D[接受连接(服务器端)]:::process
    A --> E[发起连接(客户端)]:::process
    D --> F[数据收发]:::process
    E --> F
    F --> G[关闭套接字]:::process

通过对这些知识的学习和实践,我们可以更好地利用Linux系统的强大功能,开发出高效、稳定的应用程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值