有一些函数的返回类型改掉了,具体改了哪些忘了
1.fork()
作用:调用 fork() 后,系统会创建一个与当前进程(称为父进程)几乎完全相同的新进程(称为子进程)。
● 返回值区分父子进程:
○ 在父进程中,fork() 返回子进程的进程 ID(PID)。
○ 在子进程中,fork() 返回 0。
○ 若创建失败,返回 -1。
● 执行流程:
undefined 父进程调用 fork(),系统创建子进程。
undefined 父子进程从 fork() 调用的下一行代码开始并行执行。
undefined 通过 pid 的值区分当前是父进程还是子进程。
1.1fork () 的关键特性
写时复制(Copy-on-Write, COW)
子进程不会立即复制父进程的全部内存,而是与父进程共享内存页。只有当其中一个进程修改内存时,系统才会为修改的页面创建副本,避免不必要的资源消耗。
文件描述符共享
子进程会继承父进程打开的文件描述符(如套接字、文件句柄),但父子进程对描述符的操作是独立的(如关闭一个进程的描述符不影响另一个)。这里可以理解为和共享指针一样,子进程复制父进程的文件描述符表,这些描述符指向内核中同一文件表项。文件表项有引用计数,当计数为0才会真正的关闭文件
进程地址空间隔离
父子进程有独立的地址空间,修改一个进程的变量不会影响另一个进程。
1.2 举个例子:
假设父进程已打开监听套接字(fd=3),接受新连接后 fd=4 指向客户端
调用fork()创建子进程后:
- 父进程关闭客户端 fd=4:
○ 父进程的描述符表中 fd=4 被标记为 “关闭”。
○ 内核中客户端连接的引用计数减 1(仍为 1,因为子进程还持有 fd=4)。 - 子进程关闭监听 fd=3:
○ 子进程的描述符表中 fd=3 被标记为 “关闭”。
○ 内核中监听套接字的引用计数减 1(仍为 1,因为父进程还持有 fd=3)。 - 最终结果:
○ 父进程专注监听新连接(持有 fd=3)。
○ 子进程专注处理客户端通信(持有 fd=4)。
○ 资源管理清晰,互不干扰。
2.僵尸进程
定义:是指已经结束运行,但其退出状态信息尚未被父进程读取(回收)的进程。它本质上是一个已经死亡的进程在进程表中的残留条目。
2.1过程
:
1.子进程终止通过调用exit()或者main函数返回,结束运行时,内核会向父进程发送一个SIGCHLD信号,通知父进程该子进程已终止
2.内核等待父进程,它会存一些状态码的东西(等待父进程把子进程的尸体拿去烧了)这时子进程会变成僵尸进程。
3.父进程调用wait()或者waitpid(),来:
● 读取子进程的退出状态码(了解子进程为什么以及如何终止)。
● 释放内核为该僵尸进程保留的所有信息(主要是进程表中的条目)。
● 系统回收该僵尸进程的 PID,使其可以被重新分配给新进程。
特殊情况
父进程比子进程先终止,或者崩溃了。init进程(PID=1)的会收养,然后定期调用wait()释放。
单个的僵尸进程危害很小,但是大量僵尸进程会占用PID资源。所以在服务端需要考虑处理。
3.代码
多进程实现就是用fork创建子进程然后并行执行,根据PID的值决定运行子进程逻辑还是父进程逻辑。
这里来解释如何防止僵尸线程的,涉及到的代码如下:
3.1 pid_t waitpid(pid_t pid, int *status, int options);
作用:等待pid子进程终止并回收其资源(避免僵尸进程),同时获取子进程的退出状态(status)
返回值:
● >0:成功回收的子进程 PID。
● 0:若指定 WNOHANG 且没有子进程终止。
● -1:出错(如没有子进程、权限不足)
pid:
● pid > 0:等待 PID 等于 pid 的子进程。
● pid = 0:等待当前进程组的任意子进程。
● pid < -1:等待进程组 ID 等于 abs(pid) 的子进程。
● pid = -1:等待任意子进程终止(不指定具体 PID)。
status
用于存储子进程的退出状态
● WIFEXITED(status):子进程是否正常退出(通过 exit() 或 return)。
● WEXITSTATUS(status):获取正常退出的状态码(如 exit(10) 中的 10)。
● WIFSIGNALED(status):子进程是否被信号终止。
● WTERMSIG(status):获取终止子进程的信号编号。
options
● 0:阻塞等待,直到有子进程终止。
● WUNTRACED:同时等待停止的子进程(由 SIGSTOP 等信号导致)。
● WCONTINUED:等待被停止后又继续的子进程。
● WNOHANG:非阻塞选项(no hang),若没有子进程终止,立即返回而不等待
ps:还有一个wait函数也可以用,但是功能比较单一,等价于waitpid(-1,&status,0)
ps:这个int sign,函数里也没用过啊。删了行不行?不行!因为我们要用库里的函数,把这个函数作为参数传进去,格式规定好了,所以不能删。
// 功能:回收进程,打印出回收了哪个进程
// 如果只是单纯回收不用打印,根本不用写这个函数
void read_childproc(int sign){
pid_t pid;
int status;
pid = waitpid(-1 , &status , WNOHANG);
if(WIFEXITED(status)){
cout<<"子进程返回的状态码:"<<WEXITSTATUS(status)<<endl;
cout<<"移除子进程,他的PID:"<<pid<<endl;
}
}
3.2 struct sigaction act;
作用:控制进程如何响应特定信号
struct sigaction {
// 信号处理函数(两种形式,根据 sa_flags 选择)
union {
void (*sa_handler)(int); // 普通处理函数(常用)
void (*sa_sigaction)(int, siginfo_t *, void *); // 带额外信息的处理函数
} __sigaction_handler; // 注意:这里是个 union,只能二选一
sigset_t sa_mask; // 信号掩码:处理信号时临时阻塞的信号集合
int sa_flags; // 控制信号处理行为的标志
void (*sa_restorer)(void); // 已废弃,不要使用
};
act.sa_handler = read_childproc; // 之前定义的信号处理函数
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;
sigaction(SIGCHLD, &act, NULL);
sa_handler(普通) 或 sa_sigaction(带额外信息)的处理函数。只能选一个
sa_mask:在执行信号处理函数期间,临时阻塞(屏蔽)的信号集合。防止处理当前信号时被其他信号中断。
sigemptyset(&act.sa_mask); // 清空掩码(默认不阻塞任何信号)
sigaddset(&act.sa_mask, SIGINT); // 添加 SIGINT 到掩码(处理期间阻塞 Ctrl+C)
sigdelset(&act.sa_mask, SIGQUIT); // 从掩码中移除 SIGQUIT
sa_flags:设置一些额外行为
SA_RESTART 被信号中断的系统调用(如 read、accept)自动重启
SA_NOCLDSTOP 子进程暂停(如 SIGSTOP)时不发送 SIGCHLD,仅在退出时发送
SA_NOCLDWAIT 子进程退出后直接销毁,不变成僵尸进程(即使父进程未调用 wait)
SA_SIGINFO 使用 sa_sigaction 而非 sa_handler 作为处理函数
SA_ONESHOT 处理函数仅执行一次,之后恢复为默认行为(等同于 signal() 的行为)
SA_NODEFER 处理信号期间不自动阻塞该信号本身(可能导致递归调
**int sigaction(int signum, const struct sigaction act, struct sigaction oldact)
作用:设置进程如何响应 SIGINT 信号
● signum:信号编号(使用的SIGCHLD就是内核通知父进程,子进程死啦!的编号)。
● act:指向 struct sigaction 的指针,指定新的信号处理方式。
● oldact:若不为 NULL,则存储旧的信号处理配置(用于后续恢复)。
辅助理解:struct sigaction和函数sigaction这两个的关系其实和上一节提到的socket与sock_addr的关系非常相似。上一个理解了这个就很容易理解了。
● 信号处理流程:
a. 填充 struct sigaction(设置处理函数、掩码、标志)。
b. 调用 sigaction() 应用配置。
● 网络编程流程:
a. 填充 struct sockaddr_in(设置 IP、端口、协议族)。
b. 调用 bind() 将地址绑定到套接字。
这行代码告诉操作系统内核:“将此进程的所有后续接收到的SIGCHLD信号都交给read_childproc函数处理”。这是一个进程级别的全局设置,不是函数调用后就失效。会维护在进程结构体里(PCB)里,持续存在于进程的整个生命周期。
4.粘包拆包问题
基础版已经实现这个功能了,我就是把内容拆成了两个函数放进baseutil里。
//BaseUtil
// Created by root on 2025/6/29.
//
#include "BaseUtil.h"
#include <vector>
// 把socket 接收的值放result里
int TcpBaseRead(const int& sockfd , std::string& result){
//首先判断socket描述符是否可以使用
if(sockfd == -1){
return 0;
}
char buf[BUFF_SIZE];
uint32_t net_len;
int buf_len=read(sockfd,&net_len,sizeof(net_len));
if (buf_len<=0) return buf_len;
// 转换为主机字节序
uint32_t msg_len = ntohl(net_len);
int total_body_bytes = 0;
while(total_body_bytes < msg_len){
int to_read=std::min(msg_len-total_body_bytes,static_cast<uint32_t>(BUFF_SIZE-1));
buf_len = read(sockfd , buf , to_read);
result.append(buf,buf_len);
total_body_bytes+=buf_len;
}
if (result.empty()) return 0;
return true;
}
// 把result的值写到socket里
bool TcpBaseWrite(const int& sockfd ,const std::string& result){
if(sockfd == -1) return false;
int content_len=result.size();
uint32_t net_len=htonl(content_len);// 转换成网络字节序
std::vector<char> packet(sizeof(net_len)+content_len);
// 把包头和内容全都拷贝到packet中。
memcpy(packet.data(),&net_len,sizeof(net_len));
memcpy(packet.data()+sizeof(net_len),result.data(),content_len);
int write_victory=write(sockfd,packet.data(),packet.size());
if (write_victory==0) {
return false;
}
else if (write_victory<0) {
cout << "写的不对" << endl;
return false;
}
return true;
}
//TcpClient.cpp
int TcpClient::Read(){
if(clnt_sock == -1){
cout<<"客户端未开启socket"<<endl;
return 0;
}
string result="";
int flag=TcpBaseRead(clnt_sock, result);
if (flag<=0) {
cout << "【" << clnt_sock << "】断开了连接" << endl;
return 0;
}
cout << "【" << clnt_sock << "】说:" << result << endl;
return flag;
}
void TcpClient::Write(const string& str){
if(clnt_sock < 0){
cout<<"客户端未开启socket,写入失败!"<<endl;
return;
}
if (!TcpBaseWrite(clnt_sock, str)) {
cout << "发送消息失败,对端关闭" << endl;
}
return;
}
//TcpServer.cpp
int TcpServer::StartFork() {
while(1) {
int accept_res = Accept();
cout << "【" << accept_res << "】连接成功..." << endl;
pid_t pid = fork();
if(pid < 0) {
perror("fork失败");
close(accept_res);
continue;
}
if(pid == 0) {
// 子进程
cout<<"子进程开启,将对连接【"<<accept_res<<"】提供通信服务"<<endl;
//子进程的操作区域
//关闭父进程中服务端的socket
close(serv_sock);
// 循环处理客户端消息
while (true) {
string result = "";
if (TcpBaseRead(accept_res, result)<=0) {
cout << "【" << accept_res << "】断开了连接" << endl;
break;
}
cout << "【" << accept_res << "】说:" << result << endl;
if (!TcpBaseWrite(accept_res, result)) {
cout << "发送消息失败,对端关闭" << endl;
break;
}
}
cout << "【" << accept_res << "】断开了连接" << endl;
close(accept_res);
exit(0); // 子进程退出
}
close(accept_res); // 父进程关闭客户端socket
}
return 0;
}
总结一下:
所有的连接请求来了都是先父进程里的listen_fd2(监听套接字)听到了,就调用accept(),accept问内核要fd(客户端套接字),要到了fd4就创建一个子进程,把fd4给子进程,父进程fd4就不要了。子进程listen_fd2也不要了。子进程专心和fd4对应的客户端通信。这时父进程的listen->fd2可能又听到,调用accept(),accept问内核要fd5(客户端套接字)…这时fd4的客户端通信结束了,内核知道子进程结束了,内核给父进程发信号。3.2说了调用什么已经写进程块里了。所以不管父进程代码走到哪儿都能马上调用之前预定的函数。父进程开始回收子进程PID.
其实还有问题:如果客户端只连服务端一直不输内容呢?难道我服务器这边的子进程就一直等着它?所以是不是应该加个超时机制比如多久没回应我就断开。还有,万一我发送的内容有一部分很复杂呢?服务端算不过来一直卡在那儿,那我后面的内容都做不了了,是不是应该加个跳过?但我决定先放过自己,想想就头大。看看后面再说。