Linux多线程编程进阶:fork与锁的交互及网络编程入门

引言

在多线程编程中,我们经常会遇到一个特殊的情况:多线程程序调用fork()创建子进程。当多线程程序执行fork时,子进程会继承父进程的哪些资源?锁的状态会被复制吗?这些问题在实际开发中非常重要,但往往容易被忽视。

此外,多线程之后,我们将进入另一个重要的领域------网络编程。今天,我将从多线程与fork的交互开始,逐步过渡到网络编程的基础知识,包括IP地址、端口号、网络协议分层模型,以及TCP服务器和客户端的基本实现。


第一部分:多线程与fork的交互

一、fork的基本回顾

在Linux中,fork()用于创建一个新的进程,该进程是调用进程的副本。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* thread_func(void* arg) {
    for (int i = 0; i < 5; i++) {
        printf("子线程执行中,PID=%d\n", getpid());
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    
    for (int i = 0; i < 5; i++) {
        printf("主线程执行中,PID=%d\n", getpid());
        sleep(1);
    }
    
    pthread_join(tid, NULL);
    return 0;
}

运行结果:

主线程执行中,PID=4150
子线程执行中,PID=4150
主线程执行中,PID=4150
子线程执行中,PID=4150
...

主线程和子线程的PID相同,因为它们属于同一个进程。

二、多线程程序执行fork

如果在多线程程序中执行fork(),会发生什么?

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>

void* thread_func(void* arg) {
    for (int i = 0; i < 5; i++) {
        printf("子线程执行中,PID=%d\n", getpid());
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    
    // 在创建子线程后执行fork
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        printf("子进程:PID=%d\n", getpid());
        sleep(2);
        printf("子进程结束\n");
    } else {
        // 父进程
        printf("父进程:PID=%d,子进程PID=%d\n", getpid(), pid);
        wait(NULL);
        printf("父进程结束\n");
    }
    
    pthread_join(tid, NULL);
    return 0;
}

观察结果:

  • 父进程中,主线程和子线程都在运行(共2条执行路径)

  • 子进程中,只有一条执行路径(父进程执行fork时所在的线程)

  • 子进程中的线程数量与父进程执行fork时的执行路径数量有关

三、核心结论

四、fork与锁的交互

多线程程序中使用锁变量时,执行fork()后会出现特殊情况。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>

pthread_mutex_t mutex;

void* thread_func(void* arg) {
    pthread_mutex_lock(&mutex);
    printf("线程加锁成功,持有锁5秒\n");
    sleep(5);
    pthread_mutex_unlock(&mutex);
    printf("线程解锁\n");
    return NULL;
}

int main() {
    pthread_t tid;
    
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&tid, NULL, thread_func, NULL);
    
    // 等待线程加锁成功
    sleep(1);
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        printf("子进程尝试加锁...\n");
        pthread_mutex_lock(&mutex);
        printf("子进程加锁成功\n");
        pthread_mutex_unlock(&mutex);
        printf("子进程结束\n");
    } else {
        // 父进程
        wait(NULL);
        printf("父进程结束\n");
    }
    
    pthread_join(tid, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}

运行结果分析:

  • 父进程中,线程成功加锁并持有5秒

  • fork时,父进程中锁处于被加锁状态

  • 子进程会复制父进程的锁及其状态(子进程的锁也处于被加锁状态)

  • 父进程解锁后,子进程的锁仍然处于被加锁状态(因为它们是不同的锁)

  • 子进程尝试加锁时会永远阻塞(死锁)

五、死锁的概念

死锁是指多个线程在运行过程中,因争夺资源而造成的一种互相等待的现象。在无外力干预的情况下,这些线程将永远无法继续执行。

死锁示例:

线程A 线程B
│ │
├── 持有锁1 ├── 持有锁2
│ │
├── 请求锁2 ──────→ │
│ 阻塞等待 │
│ ├── 请求锁1 ──────→ 阻塞等待
│ │
▼ ▼
两个线程互相等待对方释放锁,形成死锁

第二部分:网络编程入门

一、网络与网络设备

网络:将不同的主机通过传输介质和网络设备连接起来,实现资源共享和数据通信。

设备 功能
交换机 连接同一网络内的设备,转发数据
路由器 连接不同网络,在不同网络间转发数据
集线器 已淘汰,功能类似交换机但效率低

传输介质:

  • 双绞线(网线)

  • 光纤(速度快)

  • 同轴电缆

  • 无线(电磁波,如WiFi)

二、IP地址

IP地址用于唯一标识网络中的一台主机。

IPV4地址:

  • 32位(4字节)

  • 点分十进制表示,如 192.168.226.129

  • 每个字段取值范围:0~255

  • 由网络号 + 主机号组成

IP地址分类:

类别 开头二进制 网络号位数 主机号位数 范围
A类 0 7位 24位 0.0.0.0 ~ 127.255.255.255
B类 10 14位 16位 128.0.0.0 ~ 191.255.255.255
C类 110 21位 8位 192.0.0.0 ~ 223.255.255.255
D类 1110 组播地址 224.0.0.0 ~ 239.255.255.255

特殊IP地址:

  • 127.0.0.1:本地回环地址,表示本主机

  • 0.0.0.0:表示所有网络接口

查看IP地址的命令:

  • Linux:ifconfigip addr

  • Windows:ipconfig

IPV6地址:

  • 128位

  • 冒号分隔的十六进制数

  • 数量充足,理论上地球表面每平方厘米都有多个地址

三、端口号

IP地址标识主机,端口号标识主机上的进程。

IP地址(定位主机) + 端口号(定位进程)= 唯一的网络进程标识

端口范围 类型 说明
0~1023 知名端口 系统预留,需管理员权限(如HTTP:80,SSH:22)
1024~49151 注册端口 常用服务(如MySQL:3306)
49152~65535 动态端口 临时分配,可随意使用

四、网络协议与分层模型

协议:通信双方共同遵守的标准和规则。

OSI七层模型(理论模型):

层数 名称 功能
7 应用层 用户接口(HTTP、FTP、SMTP)
6 表示层 数据格式转换、加密解密
5 会话层 建立、管理、终止会话
4 传输层 端到端可靠传输(TCP、UDP)
3 网络层 路由选择、IP寻址
2 数据链路层 相邻节点间帧传输
1 物理层 比特流传输

TCP/IP四层模型(实际使用):

层数 名称 协议
4 应用层 HTTP、FTP、SSH
3 传输层 TCP、UDP
2 网络层 IP、ICMP
1 网际接口层 以太网、WiFi

五、TCP与UDP协议

特性 TCP UDP
连接性 面向连接 无连接
可靠性 可靠(确认重传) 不可靠
速度
适用场景 文件传输、网页访问 实时音视频、DNS查询

第三部分:TCP编程流程

一、TCP服务端编程流程

二、TCP客户端编程流程

三、服务端代码实现

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 绑定IP地址和端口
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0
    
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind error");
        close(listen_fd);
        exit(1);
    }
    
    // 3. 创建监听队列
    if (listen(listen_fd, 5) == -1) {
        perror("listen error");
        close(listen_fd);
        exit(1);
    }
    
    printf("服务器启动成功,等待连接...\n");
    
    while (1) {
        // 4. 接受客户端连接
        client_len = sizeof(client_addr);
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_fd == -1) {
            perror("accept error");
            continue;
        }
        
        printf("客户端连接成功,IP: %s, 端口: %d\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port));
        
        // 5. 接收数据
        memset(buffer, 0, BUFFER_SIZE);
        int n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0);
        if (n > 0) {
            printf("收到数据: %s\n", buffer);
            
            // 发送响应
            send(client_fd, "OK", 2, 0);
        }
        
        // 6. 关闭连接
        close(client_fd);
    }
    
    close(listen_fd);
    return 0;
}

四、客户端代码实现

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 6000
#define BUFFER_SIZE 128

int main() {
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];
    
    // 1. 创建套接字
    sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket error");
        exit(1);
    }
    
    // 2. 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect error");
        close(sock_fd);
        exit(1);
    }
    
    printf("连接服务器成功\n");
    
    // 3. 发送数据
    printf("请输入消息: ");
    fgets(buffer, BUFFER_SIZE, stdin);
    buffer[strlen(buffer) - 1] = '\0';
    
    send(sock_fd, buffer, strlen(buffer), 0);
    
    // 4. 接收响应
    memset(buffer, 0, BUFFER_SIZE);
    recv(sock_fd, buffer, BUFFER_SIZE - 1, 0);
    printf("服务器响应: %s\n", buffer);
    
    // 5. 关闭连接
    close(sock_fd);
    
    return 0;
}

五、运行与测试

编译

gcc server.c -o server

gcc client.c -o client

运行顺序:先启动服务器,再启动客户端

./server

在另一个终端

./client

关键点:

  • 必须先运行服务器,再运行客户端

  • 服务器和客户端必须同时运行

  • 本机测试使用 127.0.0.1

六、字节序转换函数

网络中统一使用大端字节序(网络字节序)。需要转换函数:

函数 功能
htons() 主机字节序 → 网络字节序(短整型,用于端口)
htonl() 主机字节序 → 网络字节序(长整型,用于IP地址)
ntohs() 网络字节序 → 主机字节序(短整型)
ntohl() 网络字节序 → 主机字节序(长整型)

IP地址转换:

cpp 复制代码
// 点分十进制字符串 → 网络字节序整数
in_addr_t inet_addr(const char* cp);

// 网络字节序整数 → 点分十进制字符串
char* inet_ntoa(struct in_addr in);

总结

一、多线程与fork核心要点

知识点 结论
fork后执行路径 只保留执行fork的那条路径
锁的复制 锁及其状态会被复制到子进程
父子进程锁关系 独立的锁,互相不影响
死锁条件 互斥、不可剥夺、请求与保持、循环等待

二、网络核心概念

概念 说明
IP地址 唯一标识主机(32位IPv4)
端口号 唯一标识进程(16位)
TCP 面向连接、可靠、面向字节流
UDP 无连接、不可靠、面向报文

三、TCP编程函数速查

函数 服务端 客户端 说明
socket() 创建套接字
bind() 绑定地址
listen() 创建监听队列
accept() 接受连接
connect() 连接服务器
recv()/send() 收发数据
close() 关闭连接

写在最后

本文分为两大部分:

  1. 多线程与fork:重点掌握了多线程程序中执行fork时,子进程只有一条执行路径,以及锁会被复制并可能导致死锁的结论。

  2. 网络编程入门:从IP地址、端口号、协议分层等基础概念,到TCP服务器和客户端的完整实现,为后续高并发网络编程打下基础。

作业要求:

  • 编写并运行TCP服务器和客户端程序

  • 确保客户端能够连接服务器,发送数据并接收响应

相关推荐
我不是立达刘宁宇2 小时前
权限提升-前置基础-linux
linux·运维·服务器
IOT.FIVE.NO.12 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库
TOWE technology2 小时前
EN32/G2401FCI——32A大功率,24位国标输出的高密度配电方案
linux·服务器·网络·科技·数据中心·pdu·智能pdu
johnny23310 小时前
运维管理面板:AcePanel、OpenOcta、DeepSentry
运维
青梅橘子皮10 小时前
Linux---基本指令
linux·运维·服务器
REDcker11 小时前
Linux信号机制详解 POSIX语义与内核要点 sigaction与备用栈实践
linux·运维·php
cui_ruicheng12 小时前
Linux进程间通信(三):System V IPC与共享内存
linux·运维·服务器
蚰蜒螟12 小时前
深入 Linux 内核同步机制:从 futex 到 spinlock 的完整旅程
linux·windows·microsoft
运维全栈笔记12 小时前
Linux安装配置Tomcat保姆级教程:从部署到性能调优
linux·服务器·中间件·tomcat·apache·web