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:ifconfig 或 ip 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服务器和客户端程序

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

相关推荐
未济3 天前
linux 配置环境变量
linux
傲世仙尊3 天前
目录即文件-Ext文件系统收尾篇
linux·c语言
虎头金猫3 天前
4K 视频总卡在公网带宽?用 N1 + OpenList 把网盘播放链路重新理顺
运维·服务器·网络·python·容器·beautifulsoup·pandas
_艾伦 耶格尔.3 天前
进程间通信
linux
AI职业加油站3 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
Liuqy-053 天前
Linux IO编程——静态库、动态库
linux
此冬歌咏3 天前
K8s 节点故障实战:优雅驱逐 31 秒,硬故障 331 秒,以及那个永远 Pending 的 Pod
运维·k8s
彧azz3 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
-梅3 天前
linux(8) 软硬链接
linux·运维·服务器
Wang's Blog3 天前
Java 项目实战: 外卖平台-文件下载与ServletOutputStream回写浏览器
服务器·项目开发