引言
在多线程编程中,我们经常会遇到一个特殊的情况:多线程程序调用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() |
✅ | ✅ | 关闭连接 |
写在最后
本文分为两大部分:
-
多线程与fork:重点掌握了多线程程序中执行fork时,子进程只有一条执行路径,以及锁会被复制并可能导致死锁的结论。
-
网络编程入门:从IP地址、端口号、协议分层等基础概念,到TCP服务器和客户端的完整实现,为后续高并发网络编程打下基础。
作业要求:
-
编写并运行TCP服务器和客户端程序
-
确保客户端能够连接服务器,发送数据并接收响应