网站建设公司不挣钱的原因,深圳优化公司样高粱seo,网站源码素材,wordpress有什么好玩的插件文章目录 日志类#xff08;完成TCP/UDP套接字常见连接过程中的日志打印#xff09;单进程版本的服务器客户端通信多进程版本和多线程版本守护进程化的多线程服务器 日志类#xff08;完成TCP/UDP套接字常见连接过程中的日志打印#xff09;
为了让我们的代码更规范化完成TCP/UDP套接字常见连接过程中的日志打印单进程版本的服务器客户端通信多进程版本和多线程版本守护进程化的多线程服务器 日志类完成TCP/UDP套接字常见连接过程中的日志打印
为了让我们的代码更规范化所以搞出了日志等级分类常见的日志输出等级有 Info Debug Warning Error Fatal 等再配合上程序运行的时间输出的内容等公司中就是使用日志分类的方式来记录程序的输出方便程序员找bug。 实际上在系统目录/var/log/messages文件中也记录了Linux系统自己的日志输出可以看到我的Linux系统中之前在使用时产生了很多的error和warning我们的代码也可以搞出来这样的输出日志信息到文件或者显示器的功能。
#pragma once
#include iostream
#include string
#include stdio.h
#include time.h
#include stdarg.h
#include sys/types.h
#include sys/stat.h
#include fcntl.h
#include string.h
#include unistd.h#define SIZE 1024
#define Screen 1 // 向屏幕打印
#define oneFile 2 // 向一个文件中打印
#define classFile 3 // 分类打印
#define LogFileName log.txt
enum
{Info 0, // 信息Debug, // 调试Warning,Error,Fatal // 严重错误
};class Log
{
private:int _printMethod;public:Log(){_printMethod Screen;}~Log(){}// 设置打印方式void Enable(int method){_printMethod method;}// 将日志等级转化为stringstd::string LevelToSting(int level){switch (level){case Info:return Info;case Debug:return Debug;case Warning:return Warning;case Error:return Error;case Fatal:return Fatal;default:return None;}}// 向一个文件中打印void PrintfOneFile(const std::string filename, const std::string logtxt) // log.txt{int fd open(filename.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}// 分类打印void PrintfClassFile(int level, const std::string logtxt) // log.txt.Info/Debug/Error等等{std::string filename LogFileName;filename .;filename LevelToSting(level);PrintfOneFile(filename, logtxt);}void printlog(int level, std::string logtxt){switch (_printMethod){case Screen:{std::cout logtxt std::endl;break;}case oneFile:{PrintfOneFile(LogFileName, logtxt);break;}case classFile:{PrintfClassFile(level, logtxt);break;}default:break;}}// 将日志信息写入到screen \ filevoid LogMessage(int level, const char *format, ...){char LeftBuffer[SIZE];time_t t time(NULL);struct tm *ctime localtime(t);snprintf(LeftBuffer, sizeof(LeftBuffer), [%s]:[%d-%d-%d %d:%d:%d], LevelToSting(level).c_str(), ctime-tm_year 1900, ctime-tm_mon 1, ctime-tm_mday, ctime-tm_hour, ctime-tm_min, ctime-tm_sec);char RightBuffer[SIZE];va_list list;va_start(list, format); // 将list指向可变参数的第一个参数vsnprintf(RightBuffer, sizeof(RightBuffer), format, list); // 这个函数按照调用者传过来的format格式执行list的可变参数部分va_end(list); //将list置NUllchar logtxt[2 * SIZE];snprintf(logtxt, sizeof(logtxt), %s %s, LeftBuffer, RightBuffer);// 现在将Log打印到stdout// printf(%s, logtxt);printlog(level, logtxt);}
};上面的localtime()是Linux中将时间戳转化本地时间的API函数会返回一个结构struct tm *这个结构里面的成员就是年月日-时分秒这个API的参数是本机的时间戳使用time(NULL)snprintf是按照格式将指定内容和长度写入到指定缓冲区va_list 是 C 语言中用于处理可变参数列表的数据类型。在使用可变参数函数如 printf、vprintf、fprintf、vfprintf 等时需要使用 va_list 类型的变量来访问这些参数。 通常你会在函数中声明一个 va_list 类型的变量然后使用一系列宏来访问可变参数列表中的参数。在使用完之后需要调用相应的宏来清理 va_list 变量。 4. vsnprintf是一个 C 标准库函数用于格式化字符串并将结果输出到字符数组中。它类似于 snprintf但是接受一个 va_list 类型的参数允许处理可变参数列表。通过 vsnprintf你可以将格式化后的字符串输出到指定的字符数组中而不需要提前知道可变参数的数量。 单进程版本的服务器客户端通信
TCP套接字的创建和UDP一样先使用socket创建套接字在结构中设置IP和port其次就是将IP 和 端口的bind 不同点是bind之后需要将套接字设置为监听状态因为TCP协议是面向连接的 监听函数success的返回0错误则返回-1错误码被设置在UDPbind完成套接字之后就是recvfrom接受客户端发过来的数据其次就是sendto 将消息处理后发回客户端。但是在TCP将套接字设置为监听状态之后需要accept接收客户端连接请求并且返回一个新的sockfd文件描述符这个新的套接字用于与客户端进行通信而原始的监听套接字仍然可以继续接受其他客户端的连接请求。那么我们使用socketAPI创建套接字的时候这个API返回的sockfd和我们使用accept返回的sockfd有什么区别呢 使用socketAPI创建的套接字属于监听套接字也就是说listenAPI需要使用它它不能进行网络通信使用accept接收的套接字这才是我们进行网络通信的套接字如果是多线程或者多进程版本的服务器我们就会使用监听套接字来进行另一个客户端的accept在TCP套接字编程中使用read 和 write 进行读写数据 //TcpSever.hpp
#pragma once
#include iostream
#include sys/types.h
#include sys/socket.h
#include string
#include Log.hpp
#include arpa/inet.h //struct sockaddr_in 结构在这个头文件里面
#include unistd.h
#include signal.h
#include pthread.hconst uint16_t default_port 8080;
const std::string default_ip 0.0.0.0;
Log lg;class TcpSever
{
private:int _listen_sockfd;uint16_t _port;std::string _ip;
public:TcpSever(uint16_t port default_port, std::string ip default_ip) : _port(port), _ip(ip){}~TcpSever(){}void Init(){// 创建tcp套接字_listen_sockfd socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd 0){lg.LogMessage(Fatal, socket Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, socket success: %d, _listen_sockfd);// 设置端口的IPstruct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);inet_aton(_ip.c_str(), (local.sin_addr));// 绑定套接字if (bind(_listen_sockfd, (struct sockaddr *)local, sizeof(local)) 0){lg.LogMessage(Fatal, bind error, errno: %d, errstring: %s, errno, strerror(errno));exit(-1);}lg.LogMessage(Info, bind socket success, listensock_: %d, _listen_sockfd);// 将套接字设置为监听状态if (listen(_listen_sockfd, 10) 0){lg.LogMessage(Fatal, listen Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, listen success);}void Service(int sockfd, uint16_t clientport, const std::string clientip){char buffer[4096];while (true){ssize_t n read(sockfd, buffer, sizeof(buffer));if (n 0){buffer[n] 0;std::cout client say# buffer std::endl;std::string echo_string server echo# ;echo_string buffer;write(sockfd, echo_string.c_str(), echo_string.size());}// 如果客户端提前退出服务端会读取到0else if (n 0){lg.LogMessage(Info, %s:%d quit, server close sockfd: %d, clientip.c_str(), clientport, sockfd);break;}else{lg.LogMessage(Warning, read error, sockfd: %d, client ip: %s, client port: %d, sockfd, clientip.c_str(), clientport);break;}}}void Run(){struct sockaddr_in client;socklen_t len sizeof(client);while (true){// 接收客户端连接 返回通信套接字struct sockaddr_in client;socklen_t len sizeof(client);int sockfd accept(_listen_sockfd, (struct sockaddr *)client, len);if (sockfd 0){lg.LogMessage(Warning, accept error, errno: %d, errstring: %s, errno, strerror(errno)); //?continue;}// 接收数据// 拿到客户端的 IP地址 和 端口uint16_t clientport ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip));// version 1 单进程版本 只能有一个用户进程进行读写Service(sockfd, clientport, clientip);close(sockfd);}}
};我们在Main.cc中创建一个服务器对象然后进行初始化 和 运行服务器端 使用命令行参数告诉服务器端的port
//Main.cc
#include TcpSever.hpp
#includeiostream
#includememoryvoid Useage(const std::string argv)
{std::cout argv - Should Enter port 1024 std::endl;
}// ./tcpsever 8080
int main(int argc, char* argv[])
{if(argc ! 2){Useage(argv[0]);return -1;}uint port atoi(argv[1]);std::unique_ptrTcpSever tcp_sever(new TcpSever(port));tcp_sever-Init();tcp_sever-Run();return 0;
}接下来就是编写客户端代码了在TCP套接字编程中connect 函数用于向服务器发起连接请求。当客户端创建一个套接字后需要调用 connect 函数来连接到服务器的指定地址和端口。 同样客户端也是需要bind的但是不需要用户显式bind在TCP套接字编程中客户端不需要显式调用 bind 函数来绑定地址的原因主要有两点 动态选择本地端口 在客户端调用 connect 函数时系统会自动为客户端选择一个合适的本地端口并将其绑定到客户端的套接字上。这样可以确保客户端套接字与服务器端建立连接时不会与其他套接字冲突。客户端套接字的行为 客户端通常不需要在网络上提供服务而是主动连接到服务器端因此不需要像服务器端那样在特定地址上监听连接请求。客户端的套接字行为是发起连接而不是等待连接因此不需要显式绑定地址。 //TcpClient.cc
#include iostream
#include sys/types.h
#include sys/socket.h
#include string
#include Log.hpp
#include arpa/inet.h //struct sockaddr_in 结构在这个头文件里面
Log lg;
using namespace std;void Useage(const std::string argv)
{std::cout argv - Should Enter port 1024 std::endl;
}int main(int argc, char *argv[])
{if (argc ! 3){Useage(argv[0]);return -1;}// 创建套接字int sockfd socket(AF_INET, SOCK_STREAM, 0);if (sockfd 0){lg.LogMessage(Fatal, socket Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, socket success: %d, sockfd);// 建立连接struct sockaddr_in sever;socklen_t len sizeof(sever);memset(sever, 0, sizeof(sever));uint port atoi(argv[2]);std::string ip argv[1];sever.sin_family AF_INET;sever.sin_port htons(port);inet_aton(ip.c_str(), (sever.sin_addr));if (connect(sockfd, (sockaddr *)sever, len) 0){lg.LogMessage(Fatal, connect Error: %s, strerror(errno));exit(-1);}std::string message;while(true){cout client please Enter endl;getline(cin, message);write(sockfd, message.c_str(), message.size());char inbuffer[4096];int n read(sockfd, inbuffer, sizeof(inbuffer));if(n 0){inbuffer[n] 0;cout inbuffer endl;}}close(sockfd);return 0;
}客户端开始死循环运行时第一件事就是向服务器发起连接请求这个连接的工作也不难做因为客户端知道目的ip和目的port所以直接填充server结构体中的各个字段然后直接发起连接请求即可。连接成功后就可以开始通信同样的客户端也是使用read和write等接口来进行数据包的发送和接收。如果服务器读到0则说明客户端已经不写了那么如果客户端继续向服务器发消息就相当于写端向已经关闭的读端继续写入此时OS会终止掉客户端进程。 由于UDP和TCP分别是无连接和面向连接的所以两者有些许不同TCP的服务器如果挂掉客户端继续写则客户端进程会被操作系统终止掉而UDP的服务器如果挂掉客户端是可以继续写的只不过客户端发送的数据包会被简单的丢弃掉罢了。 问题提出 现在出现了一个新的问题用户1连接成功并开始通信时用户2可以连接服务器因为服务器一直处于监听状态但用户2发送的消息却并不会被服务器回显而只有当第一个用户进程被终止掉之后用户2进程才会立马回显刚刚所发送的一堆消息接下来用户2才可以正常和服务器通信这是为什么呢其实主要是因为我们的代码逻辑是串行执行的一旦服务器启动时因为是单进程所以连接一个客户端之后服务器就会陷入service的死循环无法继续循环执行accept以接收来自客户端的连接请求。而当连接的客户端终止掉之后service会读到0此时才会break跳出死循环重新执行accept建立新的连接。 所以如果想要让服务器同时建立多个连接可以通过多进程或多线程以及线程池的方式来实现。 多进程版本和多线程版本
多进程的实现方案也很简单让父进程去执行客户端连接的代码也就是执行accept的功能让fork出来的子进程执行客户端进行通信的服务代码也就是执行service创建子进程后子进程应该将自己不使用的文件描述符关闭防止子进程对父进程打开的文件进行误操作尤其是像listenSockfd这样的文件描述符如果子进程对listenSockfd进行写入什么的很有可能会导致服务器崩溃此外关闭不用的文件描述符也可以给子进程腾出来一部分文件描述符表的下标位置防止文件描述符泄露。
创建出来的子进程是需要等待的pid_t result waitpid(pid, status, WNOHANG); // 使用 WNOHANG 选项进行非阻塞等待在代码中使用非阻塞式等待是一个非常不好用的做法这会让服务器的工作主线偏离因为如果要使用非阻塞式等待则势必得通过轮询的方式来检测子进程的状态那服务器就需要一直询问子进程是否退出但我服务器的核心工作主线是接收客户端的请求并建立连接进行网络通信的啊一旦非阻塞等待服务器的性能就一定会下降因为需要一直做不必要的工作比如询问子进程状态况且waitpid还是系统调用每次循环还要陷入内核所以非阻塞式等待是一个非常不好的方案不要用他。
第一种解决方案就是让子进程fork出孙子进程子进程立马退出终止让孙子进程去提供service服务孙子进程退出时会被1号进程init进程接管回收孙子进程(孤儿进程)的资源。父进程此时就可以阻塞式等待子进程退出这个阻塞其实可以忽略不计因为一旦创建出子进程子进程就会立马退出父进程也会立马回收掉子进程的资源从而父进程可以继续向后accept其他客户端的连接请求而让孙子进程提供service服务当孙子进程退出后1号进程会回收他的资源。
第二种解决方案就比较简单轻松可以直接捕捉SIGCHLD信号显示设置为SIG_IGN这样会直接忽略掉父进程就不需要等待子进程当子进程退出时linux系统会自动帮我们回收子进程资源父进程就省心了不用管子进程退不退出的事了把这件事丢给linux系统来干我父进程专心accept其他的客户端连接请求就OK。 SIGCHLD 信号是在子进程改变状态时如终止或停止由内核发送给父进程的信号。父进程通常会安装一个信号处理函数来处理 SIGCHLD 信号。 通常情况下SIGCHLD 信号的处理方式有以下几种 忽略信号 父进程可以选择忽略 SIGCHLD 信号。这通常意味着父进程对子进程的状态变化不感兴趣因此子进程终止后会被操作系统回收。捕获并处理信号 父进程可以安装一个信号处理函数来处理 SIGCHLD 信号。在信号处理函数中父进程可以调用 wait 或 waitpid 函数来等待子进程的终止并处理子进程的退出状态。使用信号的默认处理方式 如果父进程没有对 SIGCHLD 信号进行特殊处理那么默认情况下操作系统会将子进程的状态变化通知给父进程父进程可以通过调用 wait 或 waitpid 函数来获取子进程的退出状态。 SIGCHLD 信号的处理方式通常取决于父进程的需求以及对子进程状态变化的关注程度。在使用非阻塞式等待子进程时SIGCHLD 信号通常用于提醒父进程子进程的状态变化以便父进程可以及时处理。 //多进程
#pragma once
#include iostream
#include sys/types.h
#include sys/socket.h
#include string
#include Log.hpp
#include arpa/inet.h //struct sockaddr_in 结构在这个头文件里面
#include unistd.h
#include signal.h
#include pthread.hconst uint16_t default_port 8080;
const std::string default_ip 0.0.0.0;
Log lg;
class TcpSever; //声明一下
class ThreadData
{
public:uint16_t _port;std::string _ip;int _sockfd;TcpSever* _tsvr;ThreadData(int sockfd, uint16_t port, const std::string ip, TcpSever* ts) : _port(port), _ip(ip), _sockfd(sockfd), _tsvr(ts){}
};
class TcpSever
{
private:int _listen_sockfd;uint16_t _port;std::string _ip;
public:TcpSever(uint16_t port default_port, std::string ip default_ip) : _port(port), _ip(ip){}~TcpSever(){}void Init(){// 创建tcp套接字_listen_sockfd socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd 0){lg.LogMessage(Fatal, socket Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, socket success: %d, _listen_sockfd);// 设置端口的IPstruct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);inet_aton(_ip.c_str(), (local.sin_addr));// 绑定套接字if (bind(_listen_sockfd, (struct sockaddr *)local, sizeof(local)) 0){lg.LogMessage(Fatal, bind error, errno: %d, errstring: %s, errno, strerror(errno));exit(-1);}lg.LogMessage(Info, bind socket success, listensock_: %d, _listen_sockfd);// 将套接字设置为监听状态if (listen(_listen_sockfd, 10) 0){lg.LogMessage(Fatal, listen Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, listen success);}void Service(int sockfd, uint16_t clientport, const std::string clientip){char buffer[4096];while (true){ssize_t n read(sockfd, buffer, sizeof(buffer));if (n 0){buffer[n] 0;std::cout client say# buffer std::endl;std::string echo_string server echo# ;echo_string buffer;write(sockfd, echo_string.c_str(), echo_string.size());}// 如果客户端提前退出服务端会读取到0else if (n 0){lg.LogMessage(Info, %s:%d quit, server close sockfd: %d, clientip.c_str(), clientport, sockfd);break;}else{lg.LogMessage(Warning, read error, sockfd: %d, client ip: %s, client port: %d, sockfd, clientip.c_str(), clientport);break;}}}void Run(){signal(SIGCHLD, SIG_IGN); // 忽略子进程的退出状态让OS自动回收子进程僵尸struct sockaddr_in client;socklen_t len sizeof(client);while (true){// 接收客户端连接 返回通信套接字struct sockaddr_in client;socklen_t len sizeof(client);int sockfd accept(_listen_sockfd, (struct sockaddr *)client, len);if (sockfd 0){lg.LogMessage(Warning, accept error, errno: %d, errstring: %s, errno, strerror(errno)); //?continue;}// 接收数据// 拿到客户端的 IP地址 和 端口uint16_t clientport ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip))// version 2 多进程版pid_t id fork();if (id 0){close(_listen_sockfd); //子进程不需要这个fd父进程会指向fd文件Service(sockfd, clientport, clientip);close(sockfd);exit(-1);}close(sockfd); //父进程需要关闭文件描述符是引用计数的如果父进程不关闭的话用户不断增多系统中fd会不断增多}}
};多进程并不是一个好的实现方案因为创建一个进程的代价远比线程大得多频繁的创建和回收进程会给系统带来很大的压力所以多进程是一个比较重量化方案而反观多线程是一种轻量化的方案所以使用多线程能让服务器的性能消耗更小一些。 实现的方式也比较简单我们知道threadRoutine要传给pthread_create的话必须为静态方法如果是静态方法就无法调用service所以我们搞一个结构体td包含TcpServer类的指针this以及accept返回的用于通信的套接字文件描述符sockfd将td地址传递给threadRoutine函数线程函数内部进行回调serviceservice如果调用结束不要忘记将sockfd关闭避免文件描述符资源泄露。在线程这里只有阻塞式等待join和不等待两种情况没有非阻塞式等待所以主线程创建线程之后如果不想阻塞式join从线程的退出则可以创建线程之后立马将从线程设置为detach状态即线程分离线程函数执行完毕之后退出时由操作系统负责回收从线程资源主线程也就撒手不管了。 #pragma once
#include iostream
#include sys/types.h
#include sys/socket.h
#include string
#include Log.hpp
#include arpa/inet.h //struct sockaddr_in 结构在这个头文件里面
#include unistd.h
#include signal.h
#include pthread.hconst uint16_t default_port 8080;
const std::string default_ip 0.0.0.0;
Log lg;
class TcpSever; //声明一下
class ThreadData
{
public:uint16_t _port;std::string _ip;int _sockfd;TcpSever* _tsvr;ThreadData(int sockfd, uint16_t port, const std::string ip, TcpSever* ts) : _port(port), _ip(ip), _sockfd(sockfd), _tsvr(ts){}
};
class TcpSever
{
private:int _listen_sockfd;uint16_t _port;std::string _ip;
public:TcpSever(uint16_t port default_port, std::string ip default_ip) : _port(port), _ip(ip){}~TcpSever(){}void Init(){// 创建tcp套接字_listen_sockfd socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd 0){lg.LogMessage(Fatal, socket Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, socket success: %d, _listen_sockfd);// 设置端口的IPstruct sockaddr_in local;memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_port htons(_port);inet_aton(_ip.c_str(), (local.sin_addr));// 绑定套接字if (bind(_listen_sockfd, (struct sockaddr *)local, sizeof(local)) 0){lg.LogMessage(Fatal, bind error, errno: %d, errstring: %s, errno, strerror(errno));exit(-1);}lg.LogMessage(Info, bind socket success, listensock_: %d, _listen_sockfd);// 将套接字设置为监听状态if (listen(_listen_sockfd, 10) 0){lg.LogMessage(Fatal, listen Error: %s, strerror(errno));exit(-1);}lg.LogMessage(Info, listen success);}void Service(int sockfd, uint16_t clientport, const std::string clientip){char buffer[4096];while (true){ssize_t n read(sockfd, buffer, sizeof(buffer));if (n 0){buffer[n] 0;std::cout client say# buffer std::endl;std::string echo_string server echo# ;echo_string buffer;write(sockfd, echo_string.c_str(), echo_string.size());}// 如果客户端提前退出服务端会读取到0else if (n 0){lg.LogMessage(Info, %s:%d quit, server close sockfd: %d, clientip.c_str(), clientport, sockfd);break;}else{lg.LogMessage(Warning, read error, sockfd: %d, client ip: %s, client port: %d, sockfd, clientip.c_str(), clientport);break;}}}static void *Handler(void *args){ThreadData *td static_castThreadData*(args);// 为例实现客户端-服务器端 并发运行 将线程设置为分离状态 线程终止的时候会自动释放资源pthread_detach(pthread_self());// 线程之间的大多数资源是共享的所以不能不关闭文件描述符td-_tsvr-Service(td-_sockfd, td-_port, td-_ip);delete td;return nullptr;}void Run(){struct sockaddr_in client;socklen_t len sizeof(client);while (true){// 接收客户端连接 返回通信套接字struct sockaddr_in client;socklen_t len sizeof(client);int sockfd accept(_listen_sockfd, (struct sockaddr *)client, len);if (sockfd 0){lg.LogMessage(Warning, accept error, errno: %d, errstring: %s, errno, strerror(errno)); //?continue;}// 接收数据// 拿到客户端的 IP地址 和 端口uint16_t clientport ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, (client.sin_addr), clientip, sizeof(clientip));// version 3 多线程版ThreadData *td new ThreadData(sockfd, clientport, clientip, this);pthread_t tid;pthread_create(tid, 0, Handler, td);}}
};守护进程化的多线程服务器
上面的多线程服务器已经很完美了但美中不足的是只要我的xshell或者vscode关闭了该服务器就会被终止掉我们还需要重新启动服务器我们希望的是只要服务器启动之后就不再受用户登录和注销的影响这样的服务器进程我们把他叫做守护进程。 当xshell打开时Linux会为我们创建一个会话在一个会话当中有且只能有一个前台任务可以有0个或多个后台任务Linux创建的会话中刚开始都是以bash作为前台任务bash就是命令行解释器用于客户输入指令和Linux kernel进行交互当我们的程序运行起来时bash进程会自动被切换为后台进程所以你可以简单的试一下当在命令行中启动进程后执行pwdlstouch等bash指令一定是无效的因为此时bash被切到后台运行了等到进程终止退出后Linux会重新将bash从后台切换为前台进程此时用户就又可以通过bash指令重新和Linux kernel交互了。 自成一个会话的进程就被叫做守护进程也叫做精灵进程。 要想让会话关闭以后进程还在运行就需要让这个进程自成一个会话也就是成为守护进程。 系统调用setsid的作用就是将调用该函数的进程变成守护进程也就是创建一个新的会话这个会话中只有当前进程。 注意调用系统调用setsid的进程在调用之前不能是进程组的组长否则无法创建新的会话也就无法成为守护进程。 守护进程也具有文件描述符但是它们通常不会使用标准输入、标准输出和标准错误这三个标准文件描述符通常分别对应0、1和2因为守护进程通常不与终端相关联而是在后台独立运行。在守护进程启动时通常会将这些标准文件描述符重定向到/dev/null或者其他适当的文件描述符以确保它们不会产生输出或输入。 这样做的目的是为了防止在后台运行时出现意外的输出同时使得守护进程可以独立于终端运行不会受到终端的影响。 /dev/null 是一个特殊的设备文件系统中被用作无效设备。它实际上是一个位于文件系统中的文件但是任何写入到 /dev/null 的数据都会被丢弃任何从 /dev/null 读取的操作都会立即返回文件结束符。这意味着它可以用来丢弃不需要的输出或者提供一个空的输入源。 在一些情况下程序可能会输出一些信息但你并不想在这些信息中处理。这时你可以将输出重定向到 /dev/null这样输出就会被丢弃而不会在终端上显示。同样地如果程序需要一些输入但你并不想提供你也可以将输入重定向自 /dev/null这样程序会读取到空数据而不会被阻塞。总之/dev/null 是一个用于丢弃数据或提供空数据的特殊设备文件常用于重定向输入或输出到无效设备的场景中 。