Linux高性能服务器-初级部分

开头选了这本书, 看到这本书是他人总结三大本后自己写的, 先求个理解大概.
话说别人推荐的c++服务器咋都是c语言…..
2019年8月17日20:37:53

每学习一部分就写一个demo

2020年7月16日12:43:41

上面那个more标签让我想起了我第一个hexo博客的主题.
大二暑假了, 距离这篇博客写下已经将近一年了. 时间过得真快, 唯一不变的就是自己依然的菜..
也理解为什么C++服务器推荐的C语言

这里将第五章后面的内容整理下.
这算是我第一次写的博客吧, 之前写的时候写的过于详细和冗余了. 目前有了一点基础, 所以这次重新读是为了
把博客和书读薄

目前第五章后的博客有13000个汉字和44000个字母. 看看整理完毕后有多少吧

第五章Linux网络编程基础API

主机字节序和网络字节序

字节序分为 大端字节序小端字节序
由于大多数PC采用小端字节序(高位存在高地址处), 所以小端字节序又称为主机字节序

为了防止不同机器字节序不同导致的错乱问题. 规定传输的时候统一为 大端字节序(网络字节序).
这样主机会根据自己的情况决定 - 是否转换接收到的数据的字节序

基础连接

字节序转换

#include <netinet/in.h>
unsigned long int htonl (unsigned long int hostlong); // host to network long
unsigned short int htons (unsigned short int hostlong); // host to network short

unsigned long int ntohl (unsigned long int netlong);
unsigned short int ntohs (unsigned short int netlong);

点分十进制地址和网络地址转换
程序需要使用网络地址 给人察看需要使用点分十进制 灵活转换

#include <arpa/inet.h>
// 将点分十进制字符串的IPv4地址, 转换为网络字节序整数表示的IPv4地址. 失败返回INADDR_NONE
in_addr_t inet_addr(const char* strptr);

// 功能相同不过转换结果存在 inp指向的结构体中. 成功返回1 反之返回0
int inet_aton(const char* cp, struct in_addr* inp);

// 函数返回一个静态变量地址值, 所以多次调用会导致覆盖
char* inet_ntoa(struct in_addr in);

// src为 点分十进制字符串的IPv4地址 或 十六进制字符串表示的IPv6地址 存入dst的内存中 af指定地址族
// 可以为 AF_INET AF_INET6 成功返回1 失败返回-1
int inet_pton(int af, const char * src, void* dst);
// 协议名, 需要转换的ip, 存储地址, 长度(有两个常量 INET_ADDRSTRLEN, INET6_ADDRSTRLEN)
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);

socket 三部曲

// 创建 命名 监听 socket
# include <sys/socket.h>
// domain指定使用那个协议族 PF_INET PF_INET6
// type指定服务类型 SOCK_STREAM (TCP协议) SOCK_DGRAM(UDP协议)
// 有两个使用较多的选项SOCK_NONBLOCK 设置非阻塞
// SOCK_CLOEXEC 使用fork创建子进程的时候在子进程中关闭该socket
// protocol设置为默认的0
// 成功返回socket文件描述符(linux一切皆文件), 失败返回-1
int socket(int domain, int type, int protocol);

// socket为socket文件描述符
// my_addr 为地址信息
// addrlen为socket地址长度
// 成功返回0 失败返回 -1
// 这里细说还有个EADDRINUSE(被绑定的地址正在使用如TIME_WAIT状态)
// 与之搭配的解决方式 是使用setsockopt设置SO_REUSEADDR为true
int bind(int socket, const struct sockaddr* my_addr, socklen_t addrlen);

// backlog表示队列最大的长度
int listen(int socket, int backlog);

服务器

// 接受连接 失败返回-1 成功时返回socket
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)

客户端

// 发起连接
#include <sys/types.h>
#include <sys/socket.h>
// 第三个参数为 地址指定的长度
// 成功返回0 失败返回-1
int connect(int sockfd, const struct sockaddr * serv_addr, socklen_t addrlen);

连接关闭

// 关闭连接
#include <unistd.h>
// 参数为保存的socket
// 并非立即关闭, 将socket的引用计数-1, 当fd的引用计数为0, 才能关闭(需要查阅)
int close(int fd);

// 立即关闭
#include <sys/socket.h>
// 第二个参数为可选值
// SHUT_RD 关闭读, socket的接收缓冲区的数据全部丢弃
// SHUT_WR 关闭写 socket的发送缓冲区全部在关闭前发送出去
// SHUT_RDWR 同时关闭读和写
// 成功返回0 失败为-1 设置errno
int shutdown(int sockfd, int howto)
// 1. close会将引用计数-1 等到为0时才关闭套接字. shutdown则可以无视此, 激发TCP的正常连接终止序列
// 2. close会终止读和写两个方向的数据传送, 但由于TCP全双工有时需要告知对方我方发送已经完毕

基础TCP

#include<sys/socket.h>
#include<sys/types.h>

// 读取sockfd的数据
// buf 指定读缓冲区的位置
// len 指定读缓冲区的大小
// flags 参数较多
// 成功的时候返回读取到的长度, 可能小于预期长度, 需要多次读取. 读取到0 通信对方已经关闭连接, 错误返回-1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
选项名 含义 可用于发送 可用于接收
MSG_CONFIRM 指示链路层协议持续监听, 直到得到答复.(仅能用于SOCK_DGRAM和SOCK_RAW类型的socket) Y N
MSG_DONTROUTE 不查看路由表, 直接将数据发送给本地的局域网络的主机(代表发送者知道目标主机就在本地网络中) Y N
MSG_DONTWAIT 非阻塞 Y Y
MSG_MORE 告知内核有更多的数据要发送, 等到数据写入缓冲区完毕后,一并发送.减少短小的报文提高传输效率 Y N
MSG_WAITALL 读操作一直等待到读取到指定字节后才会返回 N Y
MSG_PEEK 看一下内缓存数据, 并不会影响数据 N Y
MSG_OOB 发送或接收紧急数据 Y Y
MSG_NOSIGNAL 向读关闭的管道或者socket连接中写入数据不会触发SIGPIPE信号 Y N

基础UDP

#include <sys/types.h>
#include <sys/socket.h>
// 由于UDP不保存状态, 每次发送数据都需要 加入目标地址.
// 不过recvfrom和sendto 也可以用于 面向STREAM的连接, 这样可以省略发送和接收端的socket地址
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, ing flags, const struct sockaddr* dest_addr, socklen_t addrlen);

通用读写函数

#inclued <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

struct msghdr
{
/* socket address --- 指向socket地址结构变量, 对于TCP连接需要设置为NULL*/
void* msg_name;


socklen_t msg_namelen;

/* 分散的内存块 --- 对于 recvmsg来说数据被读取后将存放在这里的块内存中, 内存的位置和长度由
* msg_iov指向的数组指定, 称为分散读(scatter read) ---对于sendmsg而言, msg_iovlen块的分散内存中
* 的数据将一并发送称为集中写(gather write);
*/
struct iovec* msg_iov;
int msg_iovlen; /* 分散内存块的数量*/
void* msg_control; /* 指向辅助数据的起始位置*/
socklen_t msg_controllen; /* 辅助数据的大小*/
int msg_flags; /* 复制函数的flags参数, 并在调用过程中更新*/
};

struct iovec
{
void* iov_base /* 内存起始地址*/
size_t iov_len /* 这块内存长度*/
}

其他Api

#include <sys/socket.h>
// 用于判断 sockfd是否处于带外标记, 即下一个被读取到的数据是否是带外数据,
// 是的话返回1, 不是返回0
// 这样就可以选择带MSG_OOB标志的recv调用来接收带外数据.
int sockatmark(int sockfd);

// getsockname 获取sockfd对应的本端socket地址, 存入address指定的内存中, 长度存入address_len中 成功返回0失败返回-1
// getpeername 获取远端的信息, 同上
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

重要 socket属性设置

/* 以下函数头文件均相同*/

// sockfd 目标socket, level执行操作协议(IPv4, IPv6, TCP) option_name 参数指定了选项的名字. 后面值和长度
// 成功时返回0 失败返回-1
int getsockopt(int sockfd, int level, int option_name, void* option_value,
socklen_t restrict option_len);
int setsockopt(int sockfd, int level, int option_name, void* option_value,
socklen_t restrict option_len);
struct linger
{
int l_onoff /* 开启非0, 关闭为0*/
int l_linger; /* 滞留时间*/
/*
* 当onoff为0的时候此项不起作用, close调用默认行为关闭socket
* 当onoff不为0 且linger为0, close将立即返回, TCP将丢弃发送缓冲区的残留数据, 同时发送一个复位报文段
* 当onoff不为0 且linger大于0 . 当socket阻塞的时候close将会等待TCP模块发送完残留数据并得到确认后关
* 闭, 如果是处于非阻塞则立即关闭
*/
};

网络信息API

#include <netdb.h>
// 通过主机名查找ip
struct hostent* gethostbyname(const char* name);

// 通过ip获取主机完整信息
// type为IP地址类型 AF_INET和AF_INET6
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

struct hostent
{
char *h_name; /* Official name of host. */
char **h_aliases; /* Alias list. */
int h_addrtype; /* Host address type. */
int h_length; /* Length of address. */
char **h_addr_list; /* List of addresses from name server. */
}

int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("非法输入\n");
exit(0);
}
char* name = argv[1];

struct hostent *hostptr{};

hostptr = gethostbyname(name);
if (hostptr == nullptr)
{
printf("输入存在错误 或无法获取\n");
exit(0);
}

printf("Official name of hostptr: %s\n", hostptr->h_name);

char **pptr;
char inet_addr[INET_ADDRSTRLEN];

printf("Alias list:\n");
for (pptr = hostptr->h_aliases; *pptr != nullptr; ++pptr)
{
printf("\t%s\n", *pptr);
}

switch (hostptr->h_addrtype)
{
case AF_INET:
{
printf("List of addresses from name server:\n");
for (pptr = hostptr->h_addr_list; *pptr != nullptr; ++pptr)
{
printf("\t%s\n",
inet_ntop(hostptr->h_addrtype, *pptr, inet_addr, sizeof(inet_addr)));
}
break;
}
default:
{
printf("unknow address type\n");
exit(0);
}
}
return 0;
}

/*
./run baidu.com
Official name of hostptr: baidu.com
Alias list:
List of addresses from name server:
39.156.69.79
220.181.38.148
*/

以下两个函数通过读取/etc/services文件 来获取服务信息 以下内容来自维基百科

Service文件是现代操作系统在etc目录下的一个配置文件,记录网络服务名对应的端口号与协议 其用途如下

  • 通过TCP/IP的API函数(声明在netdb.h中)直接查到网络服务名与端口号、使用协议的对应关系。如getservbyname(“serve”,”tcp”)获取端口号;getservbyport(htons(port),“tcp”)获取端口和协议上的服务名
  • 如果用户在这个文件中维护所有使用的网络服务名字、端口、协议,那么可以一目了然的获悉哪些端口号用于哪个服务,哪些端口号是空闲的
    #include <netdb.h>
    // 根据名称获取某个服务的完整信息
    struct servent getservbyname(const char* name, const char* proto);

    // 根据端口号获取服务信息
    struct servent getservbyport(int port, const char* proto);

    struct servent
    {
    char* s_name; /* 服务名称*/
    char ** s_aliases; /* 服务的别名列表*/
    int s_port; /* 端口号*/
    char* s_proto; /* 服务类型, 通常为TCP或UDP*/
    }
    #include <netdb.h>
    // 内部使用的gethostbyname 和 getserverbyname
    // hostname 用于接收主机名, 也可以用来接收字符串表示的IP地址(点分十进制, 十六进制字符串)
    // service 用于接收服务名, 字符串表示的十进制端口号
    // hints参数 对getaddrinfo的输出进行更准确的控制, 可以设置为NULL, 允许反馈各种有用的结果
    // result 指向一个链表, 用于存储getaddrinfo的反馈结果
    int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result)

    struct addrinfo
    {
    int ai_flags;
    int ai_family;
    int ai_socktype; /* 服务类型, SOCK_STREAM或者SOCK_DGRAM*/
    int ai_protocol;
    socklen_t ai_addrlen;
    char* ai_canonname; /* 主机的别名*/
    struct sockaddr* ai_addr; /* 指向socket地址*/
    struct addrinfo* ai_next; /* 指向下一个结构体*/
    }

    // 需要手动的释放堆内存
    void freeaddrinfo(struct addrinfo* res);
#include <netdb.h>
// host 存储返回的主机名
// serv存储返回的服务名

int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv
socklen_t servlen, int flags);

测试
使用

telnet ip port #来连接服务器的此端口
netstat -nt | grep port #来查看此端口的监听

第六章高级IO函数

Linux提供的高级IO函数, 自然是特定条件下能力更强, 不然要他干啥, 特定条件自然限制了他的使用频率
文件描述符
文件描述符在是一个非负整数。是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
STDOUT_FILENO(值为1)- 值为1的文件描述符为标准输出, 关闭STDOUT_FILENO后用dup即可返回最小可用值(目前为, 1) 这样输出就重定向到了调用dup的参数指向的文件

创建文件描述符 - pipe dup dup2 splice

pipe函数
这个函数可用于创建一个管道, 实现进程间的通信.

// 函数定义
// 参数文件描述符数组 fd[0] 读出 fd[1]写入 单向管道
// 成功返回0, 并将一对打开的文件描述符填入其参数指向的数组
// 失败返回-1 errno
#include <unistd.h>
int pipe(int fd[2]);
// 双向管道
// 第一个参数为 协议PF_UNIX(书上是AF_UNIX)感觉这里指明协议使用PF更好一些
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);

学习了后面的内容了解到了进程间通信, 回来补上一个例子

int main()
{
int fds[2];
socketpair(PF_UNIX, SOCK_STREAM, 0, fds);
int pid = fork();
if (pid == 0)
{
close(fds[0]);
char a[] = "123";
send(fds[1], a, strlen(a), 0);
}
else if (pid > 0)
{
close(fds[1]);
char b[20] {};
recv(fds[0], b, 20, 0);
printf("%s", b);
}
}

dup和dup2函数
复制一个现有的文件描述符

#include <unistd.h>
// 返回的文件描述符总是取系统当前可用的最小整数值
int dup(int oldfd);
// 可以用newfd来制定新的文件描述符, 如果newfd已经被打开则先关闭
// 如果newfd==oldfd 则不关闭newfd直接返回
int dup2(int oldfd, int newfd);

dup函数创建一个新的文件描述符, 新的文件描述符和原有的file_descriptor共同指向相同的目标.
回来补上例子, 这个例子由于关掉了STDOUT_FILENOdup最小的即为STDOUT_FILENO所以
标准输出都到了这个文件之中

int main()
{
int filefd = open("/home/lsmg/1.txt", O_WRONLY);
close(STDOUT_FILENO);
dup(filefd);
printf("123\n");
exit(0);
}

读写数据 - readv writev mmap munmap

readv/writev

#include <sys/uio.h>
// count 为 vector的长度, 即为有多少块内存
// 成功时返回写入\读取的长度 失败返回-1
ssize_t readv(int fd, const struct iovec* vector, int count);
ssize_t writev(int fd, const struct iovec* vector, int count);

struct iovec {
void* iov_base /* 内存起始地址*/
size_t iov_len /* 这块内存长度*/
}

回来补上一个使用例子, 这个例子将一个int的内存表示写入到了文件之中
使用hexdump查看这个文件0000000 86a0 0001可以看到186a0即为100000

// 2020年1月7日16:52:11
int main()
{
int file = open("/home/lsmg/1.txt", O_WRONLY);
int temp = 100000;
iovec temp_iovec{};
temp_iovec.iov_base = &temp;
temp_iovec.iov_len = sizeof(temp);
writev(file, &temp_iovec, 1);
}

sendfile函数

#include <sys/sendfile.h>
// offset为指定输入流从哪里开始读, 如果为NULL 则从开头读取
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);

O_RDONLY只读模式
O_WRONLY只写模式
O_RDWR读写模式
int open(file_name, flag);

stat结构体, 可用fstat生成, 简直就是文件的身份证

#include <sys/stat.h>
struct stat
{
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
};

身份证生成函数

// 第一个参数需要调用open生成文件描述符
// 下面其他两个为文件全路径
int fstat(int filedes, struct stat *buf);

// 当路径指向为符号链接的时候, lstat为符号链接的信息. stat为符号链接指向文件信息
int stat(const char *path, struct stat *buf);
int lstat(const char *path, struct stat *buf);

/*
* ln -s source dist 建立软连接, 类似快捷方式, 也叫符号链接
* ln source dist 建立硬链接, 同一个文件使用多个不同的别名, 指向同一个文件数据块, 只要硬链接不被完全
* 删除就可以正常访问
* 文件数据块 - 文件的真正数据是一个文件数据块, 打开的`文件`指向这个数据块, 就是说
* `文件`本身就类似快捷方式, 指向文件存在的区域.
*/

mmap和munmap函数

mmap创建一块进程通讯共享的内存(可以将文件映射入其中), munmap释放这块内存

#include <sys/mman.h>

// start 内存起始位置, 如果为NULL则系统分配一个地址 length为长度
// port参数 PROT_READ(可读) PROT_WRITE(可写) PROT_EXEC(可执行), PROT_NONE(不可访问)
// flag参数 内存被修改后的行为
// - MAP_SHARED 进程间共享内存, 对内存的修改反映到映射文件中
// - MAP_PRIVATE 为调用进程私有, 对该内存段的修改不会反映到文件中
// - MAP_ANONUMOUS 不是从文件映射而来, 内容被初始化为0, 最后两个参数被忽略
// 成功返回区域指针, 失败返回 -1
void* mmap(void* start, size_t length, int port, int flags, int fd, off_t offset);
// 成功返回0 失败返回-1
int munmap(void* start, size_t length);

splice函数
用于在两个文件名描述符之间移动数据, 0拷贝操作

#include <fcntl.h>
// fd_in 为文件描述符, 如果为管道文件描述符则 off_in必须为NULL, 否则为读取开始偏移位置
// len为指定移动的数据长度, flags参数控制数据如何移动.
// - SPLICE_F_NONBLOCK 非阻塞splice操作, 但会受文件描述符自身的阻塞
// - SPLICE_F_MORE 给内核一个提示, 后续的splice调用将读取更多的数据???????
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags);

// 使用splice函数 实现echo服务器
int main(int argc, char* argv[])
{
if (argc <= 2)
{
printf("the parmerters is wrong\n");
exit(errno);
}
char *ip = argv[1];

int port = atoi(argv[2]);
printf("the port is %d the ip is %s\n", port, ip);

int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);

struct sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);

int ret = bind(sockfd, (sockaddr*)&address, sizeof(address));
assert(ret != -1);

ret = listen(sockfd, 5);

int clientfd{};
sockaddr_in client_address{};
socklen_t client_addrlen = sizeof(client_address);

clientfd = accept(sockfd, (sockaddr*)&client_address, &client_addrlen);
if (clientfd < 0)
{
printf("accept error\n");
}
else
{
printf("a new connection from %s:%d success\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
int fds[2];
pipe(fds);
ret = splice(clientfd, nullptr, fds[1], nullptr, 32768, SPLICE_F_MORE);
assert(ret != -1);

ret = splice(fds[0], nullptr, clientfd, nullptr, 32768, SPLICE_F_MORE);
assert(ret != -1);

close(clientfd);
}
close(sockfd);
exit(0);
}

第七章Linux服务器程序规范

  • Linux程序服务器 一般以后台进程形式运行. 后台进程又称为守护进程(daemon). 他没有控制终端, 因而不会意外的接收到用户输入. 守护进程的父进程通常都是init进程(PID为1的进程)
  • Linux服务器程序有一套日志系统, 他至少能输出日志到文件. 日志这东西太重要了,排错对比全靠它.
  • Linux服务器程序一般以某个专门的非root身份运行. 比如mysqld有自己的账户mysql.
  • Linux服务器程序一般都有自己的配置文件, 而不是把所有配置都写死在代码里面, 方便后续的更改.
  • Linux服务器程序通常在启动的时候生成一个PID文件并存入/var/run 目录中, 以记录改后台进程的PID.
  • Linux服务器程序通常需要考虑系统资源和限制, 预测自己的承受能力

日志

sudo service rsyslog restart // 启动守护进程
#include <syslog.h>
// priority参数是所谓的设施值(记录日志信息来源, 默认为LOG_USER)与日志级别的按位或
// - 0 LOG_EMERG /* 系统不可用*/
// - 1 LOG_ALERT /* 报警需要立即采取行动*/
// - 2 LOG_CRIT /* 非常严重的情况*/
// - 3 LOG_ERR /* 错误*/
// - 4 LOG_WARNING /* 警告*/
// - 5 LOG_NOTICE /* 通知*/
// - 6 LOG_INFO /* 信息*/
// -7 LOG_DEBUG /* 调试*/
void syslog(int priority, const char* message, .....);

// ident 位于日志的时间后 通常为名字
// logopt 对后续 syslog调用的行为进行配置
// - 0x01 LOG_PID /* 在日志信息中包含程序PID*/
// - 0x02 LOG_CONS /* 如果信息不能记录到日志文件, 则打印到终端*/
// - 0x04 LOG_ODELAY /* 延迟打开日志功能直到第一次调用syslog*/
// - 0x08 LOG_NDELAY /* 不延迟打开日志功能*/
// facility参数可以修改syslog函数中的默认设施值
void openlog(const char* ident, int logopt, int facility);

// maskpri 一共八位 0000-0000
// 如果将最后一个0置为1 表示 记录0级别的日志
// 如果将最后两个0都置为1 表示记录0和1级别的日志
// 可以通过LOG_MASK() 宏设定 比如LOG_MASK(LOG_CRIT) 表示将倒数第三个0置为1, 表示只记录LOG_CRIT
// 如果直接设置setlogmask(3); 3的二进制最后两个数均为1 则记录 0和1级别的日志
int setlogmask(int maskpri);

// 关闭日志功能
void closelog();

用户信息, 切换用户

UID - 真实用户ID
EUID - 有效用户ID - 方便资源访问
GID - 真实组ID
EGID - 有效组ID

#include <sys/types.h>
#include <unistd.h>

uid_t getuid();
uid_t geteuid();
gid_t getgid();
gid_t getegid();
int setuid(uid_t uid);
int seteuid(uid_t euid);
int setgid(gid_t gid);
int setegid(gid_t gid);

可以通过 setuidsetgid切换用户 root用户uid和gid均为0

进程间关系

PGID - 进程组ID(Linux下每个进程隶属于一个进程组)

#include <unistd.h>
pid_t getpgid(pid_t pid); 成功时返回pid所属的pgid 失败返回-1
int setpgid(pid_t pid, pid_t pgid);

会话
一些有关联的进程组将形成一个会话
略过

查看进程关系
ps和less

资源限制

改变目录

第八章高性能服务器程序框架

服务器模型-CS模型

优点

  • 实现起来简单

缺点

  • 服务器是通信的中心, 访问过大的时候会导致响应过慢

模式图

编写的demo 没有用到fork函数. 后续待完善

服务器框架 IO模型

这个模型大概能够理解, 自己也算是学了半年的Javaweb.

socket在创建的时候默认是阻塞的, 不过可以通过传SOCK_NONBLOCK参解决
非阻塞调用都会立即返回 但可能事件没有发生(recv没有接收到信息), 没有发生和出错都会返回-1 所以需要通过errno来区分这些错误.
事件未发生
accept, send,recv errno被设置为 EAGAIN(再来一次)EWOULDBLOCK(期望阻塞)
connect 被设置为 EINPROGRESS(正在处理中)

需要在事件已经发生的情况下 去调用非阻塞IO, 才能提高性能

常用IO复用函数 select poll epoll_wait 将在第九章后面说明
信号将在第十章说明

两种高效的事件处理模式和并发模式

程序分为计算密集型(CPU使用很多, IO资源使用很少)和IO密集型(反过来).
前者使用并发编程反而会降低效率, 后者则会提升效率
并发编程有多进程和多线程两种方式

并发模式 - IO单元和多个逻辑单元之间协调完成任务的方法.
服务器主要有两种并发模式

  • 半同步/半异步模式
  • 领导者/追随者模式

半同步/半异步模式
在IO模型中, 异步和同步的区分是内核向应用程序通知的是何种IO事件(就绪事件还是完成事件), 以及由谁来完成IO读写(应用程序还是内核)

而在这里(并发模式)
同步指的是完全按照代码序列的顺序执行 - 按照同步方式运行的线程称为同步线程
异步需要系统事件(中断, 信号)来驱动 - 按照异步方式运行的线程称为异步线程

服务器(需要较好的实时性且能同时处理多个客户请求) - 一般使用同步线程和异步线程来实现,即为半同步/半异步模式
同步线程 - 处理客户逻辑, 处理请求队列中的对象
异步线程 - 处理IO事件, 接收到客户请求后将其封装成请求对象并插入请求队列

半同步/半异步模式 存在变体 半同步/半反应堆模式

异步线程 - 主线程 - 负责监听所有socket上的事件

领导者/追随者模式

高效编程方法 - 有限状态机

// 状态独立的有限状态机
STATE_MACHINE(Package _pack) {

PackageType _type = _pack.GetType();
switch(_type) {
case type_A:
xxxx;
break;
case type_B:
xxxx;
break;
}
}

// 带状态转移的有限状态机
STATE_MACHINE() {
State cur_State = type_A;
while(cur_State != type_C) {

Package _pack = getNewPackage();
switch(cur_State) {

case type_A:
process_package_state_A(_pack);
cur_State = type_B;
break;
case type_B:
xxxx;
cur_State = type_C;
break;
}
}
}

花了小一个小时 终于一个字母一个字母的抄完了那个5000多字的代码
@2019年9月8日22:08:46@

提高服务器性能的其他建议 池 数据复制 上下文切换和锁

- 用空间换取时间
进程池和线程池

数据复制 - 高性能的服务器应该尽量避免不必要的复制

上下文切换和锁
减少的作用区域. 不应该创建太多的工作进程, 而是使用专门的业务逻辑线程.


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。