webserver

webserver高性能服务器

系统:Linux

发行版:Ubuntu18

涉及知识点:

软件:VMware Station、Visual Studio Code、Xshell、Xftp

linux发行版中,有两个主流,CentOs和Ubuntu,分别是基于红帽企业和Debian,Ubuntu需要经常更新,用到.deb和apt软件包;而CentOS不常更新,使用.rpm和flatpak软件包。这说明了CentOS会更稳定。在此项目中,因为Ubuntu有一个庞大的社区,有更多的文档和免费的问题信息支持,并且Ubuntu上手更为简单,因此选择的发行版为Ubuntu18

0、搭建linux虚拟机开发环境

Ubuntu 18安装

下载Ubuntu18光驱,在VMware Station中创建Ubuntu 64位虚拟机并导入光驱进行安装,推荐安装VMware Tools,能够自适应虚拟机屏幕大小和实现在主机拖拽文件到虚拟机中等功能。

终端安装ssh,输入sudo apt install openssh-server

[报错]暂时不能解析域名“sercurity.ubuntu.com”——这是因为虚拟机没有连上网,可以打开虚拟机设置选择桥接模式,并勾上复制物理网络连接状态(复习桥接模式和NAT)

为连接虚拟机,需要获取虚拟机的IP地址,在终端输入sudo apt install net-tools安装net-tools后即可通过ifconfig命令获取IP地址

[报错]没有可安装的候选——执行sudo apt install net-tools之后再次安装即可

用xshell连接虚拟机

复制虚拟机的IP地址,在xshell中新建会话,填写会话名称和主机号(虚拟机IP地址),协议选择ssh,其余默认即可

输入用户名,密码就可以连接上

在vscode中连接虚拟机

在扩展中下载remote development,可以看到左侧栏多出的“远程资源管理器”图标,进入远程资源管理器,选择ssh并打开ssh配置文件,填写正确参数

之后,能看到左侧ssh栏下有对应的主机连接信息,选择再新窗口中打开,输入密码就能在vscode实现对虚拟机的操作

1、linux 系统编程入门

静态库创建和使用

  1. gcc -c *.c编译生成.o类型文件
  2. ar rcs libxxx.a *.o利用ar工具将.o文件生成名为xxx的静态库,其中,lib.a为固定命名

编译main.c文件时,需要包含函数声明的头文件,以及函数定义的静态库文件

文件结构如下时,

gcc -o app main.c -I ./include -l calc -L ./lib生成 可执行文件app

动态库创建和使用

  1. gcc -fpic -c *.c 编译生成与位置无关的.o文件
  2. gcc -shared *.o -o libxxx.so生成名为xxx的动态库,其中,lib.so为固定命名

在dynamic文件夹下生成动态库文件如下:

在使用动态库时,如果直接像静态库那样使用,则在运行生成的可执行文件app时会报错

使用ldd(list dynamic dependencies)命令查看动态库的依赖关系

可以看到,最终生成的可执行文件不能运行的原因在于其对应的动态库libcalc.so缺失

而文件在定位共享库时,对应ELF格式的可执行程序,先后搜索 DT_RPATH段 -> 环境变量LD_LIBRARY_PATH -> /etc/ld.so.cache 文件列表 -> /lib/, /usr/lib 目录找到库文件后将其载入内存

因此,第一种方法可以在环境变量LD_LIBRARY_PATH中加入动态库路径

但环境变量在每次关闭终端后都会恢复到打开终端之前

第二种方法则可以在用户级别下配置文件 ~/.bashrc 中保存该环境变量

vim .bashrc敲入shift+g移动到最后一行,敲入o在下一行中插入编辑

输入相同的export语句,保存并退出,. .bashrcsource .bashrc重新运行配置文件使其生效

第三种方法在系统级别下配置文件 /etc/profile 中加入同样的export语句

以上是通过环境变量来找到路径

另外,还可以修改/etc/ld.so.cache 文件,sudo vim /etc/ld.so.conf在文件中加入路径,间接修改.cache文件,保存退出,sudo ldconfig进行更新

最后,不推荐将动态库文件放到 /lib/,/usr/lib/目录中,因为这两个目录下有很多文件,可能会有重复命名问题

静态库vs动态库

静态库

  • 加载速度快
  • 程序发布不需要提供静态库,移植方便
  • 但浪费内存
  • 不利于更新部署

动态库

  • 实现进程间资源共享
  • 更新部署简单
  • 何时加载可控
  • 但加载速度慢
  • 程序发布需要提供动态库

Makefile

目标 : 依赖

​ (tab)命令

其他规则通常为第一条规则服务

1
2
app : sub.c add.c mult.c div.c main.c
gcc sub.c add.c mult.c div.c main.c -o app

依赖存在,则执行命令;否则向下检查其他规则,寻找生成依赖的命令

依赖生成时间比目标晚,则需要执行命令对目标进行更新

变量定义:变量名=变量值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AR : 归档维护程序的名称,默认值为 ar
CC : C 编译器的名称,默认值为 cc
CXX : C++ 编译器的名称,默认值为 g++
$@ : 目标的完整名称
$< : 第一个依赖文件的名称
$^ : 所有的依赖文件

%.o : %.c 匹配同一个字符串

$(wildcard ./*.c ./PATH/*.c)
返回 a.c b.c c.c
src=$(wildcard ./*.c)

$(patsubst %.c %.o a.c b.c)
返回替换a.c, b.c 后的字符串 a.o, b.o
objs=$(patsubst %.c, %.o, $(src))

.PHONY:clean
clean:
rm $(objs) -f

GDB调试工具

gcc -g -Wall a.c -o test

-g作用是在可执行文件中加入源代码信息,-wall打开所有warning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
list/l (行号)
list/l (文件名:函数名)
show list
set list (要显示的行数大小)
set args (给程序输入的参数1) (给程序输入的参数2)
show args
b (行号)
b (文件名:函数名)
b (行号) if (条件)
i b
d (断点编号)
dis (断点编号)
ena (断点编号)
run ——遇到断点才停
start ——停在第一行
c ——到下一个断点
n ——下一行(不进入函数体)
s ——下一步(进入函数体)
finish ——调出函数体
p (变量名) ——打印变量值
ptype (变量名) ——打印变量类型
display (变量名) ——自动打印变量名
undisplay(变量名) ——取消自动打印
i display ——显示自动打印信息
set var (变量名)=(变量值)

文件I/O函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* open 函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- flags: O_RDONLY, O_WRONLY, O_RDWR, O_CREAT...
- mode: 八进制,最终文件权限值为 mode & ~umask, umask 默认 0002, 也可以使用宏,如 S_IRWXU (__S_IREAD|__S_IWRITE|__S_IEXEC)
*/

/*read 函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
从fd指向的文件中读取 count字节,存到buf中
返回-1则读取失败,0则表示读取到文件末尾,否则返回读取到的字节数
size_t and ssize_t are, respectively, unsigned and signed
*/

/*write 函数
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
将buf中的count字节写入到文件fd中
返回写入的字节数,有可能小于count,因为写入空间不够。-10则写入失败
*/

文件操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

/*lseek 函数
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
改变文件 fd 中的读写指针位置
- whence 可选参数
宏定义 SEEK_SET, 文件开始位置
SEEK_CUR, 文件指针当前位置
SEEK_END, 文件结束位置
返回从文件开头开始的指针位置,-1则报错,并将报错记录在perrno中
*/

/*stat 函数
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
获取文件信息,记录在statbuf当中
- struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B 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 */
};
- 返回0则成功,否则返回-1
*/

其中,st_mode 变量如下:

setGID - 设置组id,setUID - 设置用户id,Sticky - 粘住位

st_mode

根据文件类型位,与相应位的宏定义 相与 运算即可,

1
2
3
4
5
6
7
/*lstat 函数
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int lstat(const char *pathname, struct stat *statbuf);
stat函数如果传入的是软连接,则返回链接文件信息而非链接指向的文件,而lstat函数返回链接指向文件的信息
*/

利用上述函数实现ls -l 命令

接受一个文件名当做函数参数,输出该文件的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//ls-l.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <grp.h>
#include <pwd.h>
#include <time.h>

// 类型权限 硬链接数 用户名 组名 文件大小 修改时间 文件名
//-rw-rw-r-- 1 kevin kevin 0 5月 1 16:07 ls-l.c

int main(int argc, char* argv[])
{
if(argc < 2)
{
perror("too few arguments");
return -1;
}
struct stat st;
int flag = stat(argv[1], &st);
if(flag == -1)
{
perror("stat");
return -1;
}

char filetype;
switch(st.st_mode & __S_IFMT)
{
case __S_IFLNK:
filetype = 'l';
break;
case __S_IFSOCK:
filetype = 's';
break;
case __S_IFREG:
filetype = '-';
break;
case __S_IFDIR:
filetype = 'd';
break;
case __S_IFBLK:
filetype = 'b';
break;
case __S_IFCHR:
filetype = 'c';
break;
case __S_IFIFO:
filetype = 'f';
break;
default:
filetype = '?';
break;
}
char quanxian[10];
memset(quanxian, '-', sizeof(quanxian));
// user
if(st.st_mode & S_IRUSR)
quanxian[1] = 'R';
if(st.st_mode & S_IWUSR)
quanxian[2] = 'W';
if(st.st_mode & S_IXUSR)
quanxian[3] = 'X';

// group
if(st.st_mode & S_IRGRP)
quanxian[4] = 'R';
if(st.st_mode & S_IWGRP)
quanxian[5] = 'W';
if(st.st_mode & S_IXGRP)
quanxian[6] = 'X';

// other
if(st.st_mode & S_IROTH)
quanxian[7] = 'R';
if(st.st_mode & S_IWOTH)
quanxian[8] = 'W';
if(st.st_mode & S_IXOTH)
quanxian[9] = 'X';
// printf("%c%s\n", filetype,quanxian);

// 硬链接数
int linknum = st.st_nlink;

// 组名 用户名
char* u = getpwuid(st.st_uid)->pw_name;
char* g = getgrgid(st.st_gid)->gr_name;

// 文件大小
long int fsize = st.st_size;

// 文件名
char* file_name = argv[1];

// 修改时间
char* t = ctime(&st.st_mtime); // 这个字符串包含最后的换行符
// printf("%s\n", t);
char tt[100];
// strncpy(tt, t, sizeof(t) - 1); // Tue May
strncpy(tt, t, strlen(t) - 1); // Tue May 2 10:15:54 2023
// printf("%s\n", tt);

// 最终输出结果
char disp[1024] = {0};

sprintf(disp, "%c%s %d %s %s %ld %s %s\n", filetype, quanxian, linknum, u, g, fsize, tt, file_name);
// --RW-RW-R--Tue May 2 10:23:42 2023 1 kevin kevin 2590 Tue May 2 10:23:42 2023 ls-l.c
// -rw-rw-r-- 1 kevin kevin 2668 5月 2 10:26 ls-l.c


printf("%s", disp);
return 0;
}

文件属性操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*access 函数
#include<unistd.h>
int access(const char * pathname, int mode)
判断文件的权限或是否存在
-mode, R_OK, W_OK, X_OK, F_OK
成功返回0,否则返回-1
*/

/*chmod 函数
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
改变文件权限
-mode, 八进制数,使用不同用户宏定义
*/


/*truncate 函数
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
截断或扩展文件长度
*/

目录操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*mkdir 函数
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
- 创建目录,赋予mode权限,mode是八进制数,最终权限 = mode & ~umask & 0777
*/

/*rmdir 函数
#include <unistd.h>
int rmdir(const char *pathname);
- 删除目录,目录要求是空目录
*/

/*raname 函数
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
*/

/*chdir 函数
#include <unistd.h>
int chdir(const char *path);

*/

/*getcwd 函数
#include <unistd.h>
char *getcwd(char *buf, size_t size);
- 获得当前工作目录
- buf 用于存储路径的数组,size 是数组的大小
- 返回值就是buf
*/

目录遍历函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*opendir 函数
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
- name 目录的名称
- 返回 DIR* 类型,目录流,失败返回null
*/

/*readdir 函数
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
- dirp是opendir返回值
- 返回struct dirent,代表读取到的文件的信息
读取到了末尾或者失败则返回NULL
*/

/*closedir 函数
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
*/

利用上述函数实现对目录的遍历和对该目录下普通文件数量的统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#define _DEFAULT_SOURCE
// #define _GNU_SOURCE
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int tongji(const char * path)
{
DIR* dir = opendir(path);
if(dir == NULL)
{
perror("opendir");
exit(0);
}
int cnt = 0;
struct dirent* dent;
while((dent = readdir(dir)) != NULL)
{
char* dirname = dent->d_name;
if(strcmp(dirname, ".") == 0|| strcmp(dirname, "..") == 0)
continue;
if(dent->d_type == DT_DIR)
{
char newpath[1024];
sprintf(newpath, "%s/%s", path, dirname);
cnt += tongji(newpath);
}
if(dent->d_type == DT_REG)
cnt += 1;
}
closedir(dir);
return cnt;
}
int main(int argc, char * argv[])
{
if(argc < 2)
{
perror("too few args.");
return -1;
}

int res = tongji(argv[1]);
printf("普通文件个数为:%d\n", res);
return 0;
}

文件描述符复制函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*dup 函数
#include <unistd.h>
int dup(int oldfd);
复制旧的文件描述符,返回一个新的最小空闲描述符
*/

/*dup2 函数
#include <unistd.h>
int dup2(int oldfd, int newfd);
重定向文件描述符,将原本指向文件a的描述符oldfd 转向成指向文件b的描述符 newfd
*/

/*fcntl 函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
对需要操作的文件fd进行cmd操作
- F_DUPFD : 复制文件描述符,复制fd,返回一个新的文件描述符
- F_GETFL : 获取指定的文件描述符文件状态flag,类似open函数传递的flag
- F_SETFL : 设置文件描述符文件状态flag
必选:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改
可选:O_APPEND, O_NONBLOCK非阻塞
*/

2、Linux多进程开发

进程创建与查看

1
2
3
4
5
6
7
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
成功:子进程返回0,父进程返回子进程ID
失败则返回-1
- 当前系统的进程数达到系统规定上限,errno置为EAGAIN
- 系统内存不足,errno置为ENOMEM

ps auxps ajx列出与作业控制相关的信息、ps -ef

top命令实时显示进程动态 -d指定更新时间间隔

多进程GDB调试

GDB默认只跟踪一个进程,调试当前进程时,其他进程继续运行

set follow-fork-mode [parent | child]设置当前调试父进程或子进程

set detach-on-fork [on | off] off表示调试当前进程时,其他进程被挂起,否则继续运行

info inferiors查看调试的进程

inferior id切换当前调试的进程

detach inferiors id使进程脱离GDB调试

exec函数族

exec函数族的作用是根据指定的文件名找到可执行文件,调用后直接取代当前进程的内容,调用进程覆盖原来的内存

调用成功不返回,调用失败则返回-1

1
2
3
4
5
6
7
8
9
10
11
12
13
int execl(const char *path, const char *arg, .../* (char *) NULL */);

int execlp(const char *file, const char *arg, ... /* (char *) NULL */);

int execle(const char *path, const char *arg, .../*, (char *) NULL, char *const envp[] */);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execvpe(const char *file, char *const argv[], char *const envp[]);

int execve(const char *filename, char *const argv[], char *const envp[]);

l(list)——参数地址列表,以空指针结尾
v(vector)——存有各参数地址的指针数组的地址
p(path)——按 PATH 环境变量指定的目录搜索可执行文件
e(environment)——存有环境变量字符串地址的指针数组的地址

进程控制

标准C库exit与Linux标准库_exit

1
2
3
4
5
6
#include <stdlib.h>
void exit(int status);
在终止进程之前会刷新I/O缓冲区,而_exit则不会

#include <unistd.h>
void _exit(int status);

孤儿进程:父进程在子进程之前退出,子进程就被称为孤儿进程,将成为init进程的子进程,孤儿进程没有危害

僵尸进程:子进程在父进程之前退出,内核会在子进程退出后会保留一定的信息,而父进程没有进行处理,则产生僵尸进程,大量僵尸进程占用进程号,导致无法创建新的进程

僵尸进程的解决

  1. 干掉父进程,让子进程变为孤儿进程,由init进程负责释放

  2. 父进程调用wait()waitpid()

    1
    2
    3
    4
    5
    6
    7
    8
    #include <sys/types.h>
    #include <sys/wait.h>

    pid_t wait(int *wstatus);
    - wstatus 储存子进程退出状态,使用相关宏函数可以获取退出状态信息
    - 返回值 > 0 返回的是子进程ID号, < 0 失败,所有子进程结束
    pid_t waitpid(pid_t pid, int *wstatus, int options);
    - pid = -1, options = WNOHANG时等同于wait()
  3. fork两次

    父进程fork子进程,子进程fork孙进程后退出,则孙进程被init接管,但需要父进程回收子进程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>

    int main()
    {
    pid_t pid;
    //创建第一个子进程
    pid = fork();
    if (pid < 0)
    {
    perror("fork error:");
    exit(1);
    }
    //第一个子进程
    else if (pid == 0)
    {
    //子进程再创建子进程
    printf("I am the first child process.pid:%d\tppid:%d\n",getpid(),getppid());
    pid = fork();
    if (pid < 0)
    {
    perror("fork error:");
    exit(1);
    }
    //第一个子进程退出
    else if (pid >0)
    {
    printf("first procee is exited.\n");
    exit(0);
    }
    //第二个子进程
    //睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
    sleep(3);
    printf("I am the second child process.pid: %d\tppid:%d\n",getpid(),getppid());
    exit(0);
    }
    //父进程处理第一个子进程退出
    if (waitpid(pid, NULL, 0) != pid)
    {
    perror("waitepid error:");
    exit(1);
    }
    exit(0);
    return 0;
    }
  4. signal函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    #include <stdio.h>
    #include <unistd.h>
    #include <errno.h>
    #include <stdlib.h>
    #include <signal.h>

    static void sig_child(int signo);

    int main()
    {
    pid_t pid;
    //创建捕捉子进程退出信号
    signal(SIGCHLD,sig_child);
    pid = fork();
    if (pid < 0)
    {
    perror("fork error:");
    exit(1);
    }
    else if (pid == 0)
    {
    printf("I am child process,pid id %d.I am exiting.\n",getpid());
    exit(0);
    }
    printf("I am father process.I will sleep two seconds\n");
    //等待子进程先退出
    sleep(2);
    //输出进程信息
    system("ps -o pid,ppid,state,tty,command");
    printf("father process is exiting.\n");
    return 0;
    }

    static void sig_child(int signo)
    {
    pid_t pid;
    int stat;
    //处理僵尸进程
    while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
    printf("child %d terminated.\n", pid);
    }

进程通信

进程间通讯的目的:

  1. 数据传输
  2. 通知事件
  3. 资源共享
  4. 进程控制

匿名管道

管道即匿名管道,没有文件实体,但有名管道有文件实体

管道是字节流,能够读取任意大小的数据块

匿名管道只能在具有公共祖先的进程中使用

使用命令ulimit -a可以查看管道缓冲大小

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
int pipe(int pipefd[2])
- 创建管道
- pipefd数组中,pipefd[0]表示管道读端, pipefd[1]表示写端,相当于管道两端的文件描述符

int pipe2(int pipefd[2], int flags)
- flags参数O_NONBLOCK可以将管道设为非阻塞

long fpathconf(int fd, int name)
- 查看管道缓冲大小函数
- fd是管道的读端或者写端,根据name所对应的宏定义可以获取管道的相关信息,_PC_PIPE_BUF最大缓冲区大小

有名管道

有名管道克服了匿名管道只能在有亲缘关系的进程间通信的缺点,以FIFO文件形式存在于文件系统中,只要能够访问该文件,就能彼此进行通信

作为一个特殊文件存在,但FIFO中的内容存放在内存中

与匿名管道一样,都不支持lseek()文件定位操作

可以通过mkfifo命令创建有名管道

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
- mode 与 open中一样,八进制数

内存映射

将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/mman.
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- addr = NULL, 由内核指定
- length是要映射的数据长度,可以用lseek、stat获取文件的长度
- prot是映射内存的操作权限,
PROT_EXEC,PROT_WRITE,PROT_READ,PROT_NONE,通常要小于等于磁盘文件open的权限
- flags, MAP_SHARED:映射区数据自动与磁盘文件同步 MAP_PRIVATE:不同步
- fd 映射的文件描述符
- offset 偏移量,要求是4k的整数倍,通常是0
- 返回创建的内存的首地址,失败则返回MAP_FAILD,即-1
int munmap(void *addr, size_t length);
- 释放内存映射,参数需要与mmap对应

也可以使用flags=MAP_ANONYMOUS不依赖磁盘文件,使用匿名内存映射

void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

信号

信号是在软件层次上对中断机制的一种模拟,属于异步通信

命令kill -l查看系统定义的信号列表,前31个为常规信号,其余为实时信号

SIGINT——用户按下<Ctrl+C>组合键引发——终止进程

SIGQUIT——用户按下<Ctrl+C>组合键引发——终止进程

SIGKILL——无条件终止进程,不能被忽略,处理和阻塞——终止除了僵尸以外的所有进程

SIGPIPE——管道破裂,向一个没有读端的管道写数据——终止进程

SIGSEGV——段错误,无效内存访问——终止进程并产生core文件

SIGCHLD——子进程结束时,父进程收到该信号——忽略

SIGCONT——进程停止则继续运行——忽略

SIGSTOP——停止进程的执行,不能被忽略,处理和阻塞——暂停进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);// 产生任意信号sig给进程pid
- pid = 0, 当前组;pid > 0, 进程号为pid的进程;pid = -1, 所有能接受sig信号的进程,除了init进程;pid < 0,组号为pid绝对值的进程

int raise(int sig);// 相当于kill(getpid(), sig)
void abort(void);// 相当于kill(getpid(), SIGABRT)


#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 传入0取消闹钟,传入任何值都会取消之前的闹钟
- 返回上一个闹钟剩余时间, 0 则没有闹钟
- 非阻塞,产生SIGALARM信号,默认终止当前进程

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_val,struct itimerval *old_value);
- which参数:ITIMER_REAL指真实时间,同alarm(),产生SIGALRM信号;ITIMER_VIRTUAL指内核态CPU执行时间,包括所有线程消耗的时间,产生SIGVTALRM信号;ITIMER_PROF指该进程在用户态和内核态CPU消耗的时间,产生SIGPROF信号
- struct itimerval {
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
- old_value 即getitimer()返回的指针,用于返回先前设定的值,为NULL则不需要返回先前值

信号捕捉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*ANSI C 标准*/
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- signum 捕捉的信号,整型数或宏定义
- handler 设为SIG_IGN则忽略信号;SIG_DFL则默认动作;也可以传入函数指针自定义动作
- 返回先前的信号处理函数指针,SIG_ERR则捕捉不成功
- SIGKILL和SIGSTOP不能被捕捉和忽略

/*POSIX标准*/
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
- signum 捕捉的信号
- struct sigaction {
void (*sa_handler)(int);// 自定义的操作函数指针,也可以是SIG_IGN或SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *);// 只有在sa_flags指定时才定义,与sa_handler只定义一个
sigset_t sa_mask;//自定义临时阻塞信号集
int sa_flags;// 设为0时使用sa_handler;设为SA_SIGINFO时使用sa_sigaction
void (*sa_restorer)(void);// 已废弃
};

信号集操作函数

未决信号集:未决是一种状态,指从信号产生到信号被处理前的时间

阻塞信号集:阻塞是一种动作,阻止信号被处理,但不阻止信号产生

信号集在内核使用位图实现,数据类型为sigset_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*对自定义的信号集进行操作*/
#include <signal.h>
int sigemptyset(sigset_t *set);
- 初始化信号集set所有信号为0
int sigfillset(sigset_t *set);
- 初始化信号集set所有信号为1
int sigaddset(sigset_t *set, int signum);
- 置单个信号为1
int sigdelset(sigset_t *set, int signum);
- 置单个信号为0
int sigismember(const sigset_t *set, int signum);
- 查询单个信号是否为1
- 返回1则是,0则不是,-1查询失败

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 用于获取或者改变信号掩码,即阻塞信号集
- how 设为SIG_BLOCK,阻塞信号集则是与set相或的结果,即并集;设为SIG_UNBLOCK,阻塞信号集则是与~set相与的结果;设为SIG_SETMASK,阻塞信号集则是set
- oldset如果非NULL,则用来存储修改之前的阻塞信号集
- 返回0成功,-1失败
int sigpending(sigset_t *set);
- 获取当前线程的未决信号集,存储在set中

SIGCHLD信号

SIGCHLD信号产生的条件:子进程终止;子进程收到SIGSTOP信号停止;子进程停止后收到SIGCONT信号被唤醒。父进程接收到信号后默认忽略该信号

因为信号是异步通信,使用waitpid(-1, NULL, WNOHANG)实现不阻塞父进程情况下完成对子进程的回收,以及获取到子进程的状态信息

共享内存

共享内存是在物理内存中创建一块可以由多个进程共享的区域,需要其他机制实现同步对共享内存的访问,共享内存无需内核介入,因此速度更快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
int shmget(key_t key, size_t size, int shmflg);
- key表示一个共享内存的标志,十六进制,设为IPC_PRIVATE 或者 不为IPC_PRIVATE,同时没有key对应的共享内存段,并且shmflg中指定IPC_CREAT,则创建新的共享内存段
- size共享内存的大小,向上取整到PAGE_SIZE的整数倍
- shmflg设为IPC_CREAT新建共享内存段;设为IPC_EXCL|IPC_CREAT如果已经存在,则返回错误号EEXIST
- 成功则返回共享内存id号,失败返回01
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 将共享内存与当前进程关联
- shmaddr设为NULL,系统安排段地址
- shmflg设为SHM_EXEC,共享内存可以被执行;设为SHM_RDONLY,共享内存只读;不指定(0)则为读写
- 成功则返回共享内存的首地址,失败返回-1
int shmdt(const void *shmaddr);
- 将共享内存与当前进程取消关联,更新共享内存的shmid_ds结构体
- 成功则返回0,失败返回-1
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 对共享内存进行cmd操作,设为IPC_STAT,获取当前共享内存状态;设为IPC_SET,设置共享内存状态;设为IPC_RMID,标记共享内存为销毁
- buf需要设置或者获取的共享内存的属性信息,当cmd为IPC_STAT时指定在buf中存储信息;IPC_SET根据buf指定的信息修改共享内存属性;IPC_RMID为NULL

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- 根据存在的路径名pathname和八位的整型数proj_id,生成一个共享内存的key值,相同路径名和proj_id相同时返回相同的key值

共享内存操作命令

内存映射与共享内存的区别

  1. 共享内存直接创建,内存映射需要磁盘文件
  2. 共享内存速度更快
  3. 进程操作的是同一块共享内存,内存映射在每个进程虚拟空间中有独立内存
  4. 进程突然退出共享内存依然存在,内存映射区消失;电脑死机共享内存消失,内存映射磁盘文件存在
  5. 进程退出共享内存区标记为删除,内存映射区销毁

守护进程

  • 守护进程运行在后台,独立于控制终端,内核不为守护进程自动生成任何控制信号以及终端相关的信号

  • 会周期性执行某种任务,一般采用以d结尾的名字

  • 守护进程的父进程是init进程,是孤儿进程

创建守护进程步骤

  1. 在父进程中fork并退出父进程,使子进程成为孤儿进程,此时子进程继承父进程的会话、进程组、控制终端、文件描述符等
  2. 在子进程中调用setsid创建新会话,脱离原来的进程组,会话以及终止终端。但此时可能会申请一个控制终端,因此可以再fork出子进程2,这样子进程2就不是会话组长,就不能申请控制终端了
  3. 更改当前工作目录,因为守护进程运行到系统关机,这就要求其所在目录不能被卸载
  4. 设置掩码,不受父进程的umask影响,可以设为0
  5. 关闭文件描述符,将0,1,2三个文件描述符重定向到”/dev/null”

另外,也可以利用库函数daemon()创建守护进程

1
2
3
4
5
#include <unistd.h>
int daemon(int nochdir, int noclose);
- nochdir 设为0时将当前目录更改为"/"
- noclose 设为0时将标准输入、标准输出、标准错误重定向至"/dev/null"
- 成功返回0,失败返回-1

3、Linux多线程开发

线程

线程是进程当中的一条执行流程

同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源

每个线程各自有一套独立的寄存器和栈,确保线程的控制流相对独立

使用命令ps -Lf pid查看指定进程的LWP号

线程vs进程

进程是分配资源的基本单位,线程是调度的基本单位

进程间不共享内存,需要采用进程通信方式,线程共享内存和文件资源

进程创建需要资源管理信息,内存、文件管理信息等,线程创建共享它们

进程上下文切换开销大,切换时要把页表切换掉,同一个进程内的线程切换只需要切换私有数据、寄存器等不共享数据

线程操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- 以执行函数指针start_routine指向的函数创建线程,并且向函数传递arg参数,创建的线程属性由attr指定
- thread存储新建线程的线程ID
pthread_t pthread_self(void);
- 成功返回0
int pthread_equal(pthread_t t1, pthread_t t2);
- 判断两个线程ID是否相同
void pthread_exit(void *retval);
- 当前线程退出,返回retval,能够在pthread_join函数中使用
int pthread_join(pthread_t thread, void **retval);
- 以阻塞的方式等待一个线程结束
- retval接受线程退出传递参数的地址
int pthread_detach(pthread_t thread);
- 分离一个线程,被分离的线程在终止时自动释放资源,不能连接一个分离的线程
int pthread_cancel(pthread_t thread);
- 当线程到达一个取消点时取消
- 线程取消取决于 cancelability state and type
- cancelability state由pthread_setcancelstate决定,新线程默认启用(enabled),启用后由cancelability type决定线程取消时机
- cancelability type由pthread_setcanceltype决定,新线程默认为deferred,也可以是asynchronous,deferred意味着直到下一次取消点线程才会取消
- 只有连接线程才能获取PTHREAD_CANCELED退出状态码,才能知道线程取消成功

线程属性操作函数

1
2
3
4
5
6
7
8
9
10
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
- 初始化线程属性attr
int pthread_attr_destroy(pthread_attr_t *attr);
- 释放线程属性资源
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int* detachstate);
- 获取线程分离状态的属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
- 设置线程分离状态的属性
- detachstate可设为 PTHREAD_CREATE_DETACHED或者PTHREAD_CREATE_JOINABLE(默认)

互斥与同步

互斥:一个线程在临界区执行时,其他线程应该被阻止进入临界区

同步:并发线程或进程在一些关键点上需要相互等待与互通信息

加锁解锁操作可以实现互斥,信号量可以实现互斥和同步

互斥锁Mutex

1
2
3
4
5
6
7
8
9
10
11
12
互斥量的类型 pthread_mutex_t
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
-初始化互斥锁
- attr互斥锁相关属性,可以为NULL
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥锁资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁,并且阻塞当前线程
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 尝试加锁,如果加锁失败则返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁

读写锁

如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作

如果有其他线程写数据,则其他线程不允许读或写操作

写独占,优先级更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
读写锁的类型 pthread_rwlock_t

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
-
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

条件变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
条件变量的类型 pthread_cond_t

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 初始化条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
- 释放条件变量资源
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 解除互斥锁并阻塞等待,直到条件变量发出信号;当接收到信号时将会重新加上互斥锁
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
- 解除互斥锁并阻塞等待指定时间
int pthread_cond_signal(pthread_cond_t *cond);
- 唤醒等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
- 唤醒所有等待的线程

信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
信号量的类型 sem_t
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- pshared设为0,表示线程;否则表示进程
- value信号量的初始值
int sem_destroy(sem_t *sem);
- 释放信号量资源
int sem_wait(sem_t *sem);
- 加锁,相当于P操作,信号量值value减一,如果值为0则阻塞
- 返回0成功
int sem_trywait(sem_t *sem);
- 如果无法加锁,返回-1,errorno为EAGAIN而不阻塞
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
- 超过时长abs_timeout无法加锁,返回-1,errorno为ETIMEOUT而不阻塞
int sem_post(sem_t *sem);
- 释放锁,相当于V操作,信号量值value加一
int sem_getvalue(sem_t *sem, int *sval);
- 获取信号量sem的value

4、Linux网络编程

字节序转换函数

1
2
3
4
5
6
7
8
9
//主机字节序转换成网络字节序
#include <arpa/inet.h>
// 端口号转换
uint16_t htons(uint16_t hostshort)
uint16_t ntohs(uint16_t netshort)

// IP地址转换
uint32_t htonl(uint32_t hostlong)
uint32_t ntohl(uint32_t netlong)

IP地址转换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
- af:地址族: AF_INET AF_INET6
- src:需要转换的点分十进制的IP字符串
- dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- af:地址族: AF_INET AF_INET6
- src: 要转换的ip的整数的地址
- dst: 转换成IP地址字符串保存的地方
- size:第三个参数的大小(数组的大小)
- 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

套接字函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- domain: 协议族 ,AF_INET : ipv4、AF_INET6 : ipv6、AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0,流式协议默认使用 TCP;报式协议默认使用 UDP
- 成功:返回文件描述符,操作的就是内核缓冲区,失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip + port)
- addrlen : 指定第二个参数的对应的内存大小
- 成功返回用于通信的文件描述符,失败返回-1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

端口复用

端口复用允许一个程序中多个套接字在一个端口号上绑定,但只有最后一个套接字能够接受信息;程序退出而系统没有释放端口,跳过TIME_WAIT状态

使用setsockopt函数中的SO_REUSEADDR选项

1
2
3
4
5
6
7
8
9
#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字的属性(不仅仅能设置端口复用),在服务器绑定端口之前设置,绑定到同一个端口的所有套接字都得设置复用
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd : 要操作的文件描述符
- level : 级别 设为SOL_SOCKET
- optname : 选项的名称 设为SO_REUSEADDR
- optval : 端口复用的值(整形),设为1可以复用,0则不可以
- optlen : optval参数的大小

IO多路复用

IO多路复用使程序能够同时监听多个文件描述符,能够提高程序的性能

select函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 允许程序监听多个文件描述符,直到一个或多个文件描述符有数据改动,监听数量小于 FD_SETSIZE(1024)
- readfds 指向fd_set结构的指针,监听读缓冲区是否有数据可以读入(不被阻塞)如果有则返回大于0的值,如果超出timeout的时间返回0,错误返回-1;writefds 指向fd_set结构的指针,监听写缓冲区是否可以写入,如果有则返回大于0的值,如果超出timeout的时间返回0,错误返回-1;execptfds 指向fd_set结构的指针,用来监听文件错误异常
- nfds设为最大文件描述符+1
- timeout指定阻塞监听文件描述符的时长,NULL则阻塞直到文件描述符有变化
- 监听成功返回sets中的文件描述符数量
void FD_CLR(int fd, fd_set *set);
- 从set中移除fd指定的文件描述符
int FD_ISSET(int fd, fd_set *set);
- 判断select返回的set中fd文件描述符是否为1,即有改动
void FD_SET(int fd, fd_set *set);
- 从set中添加fd指定的文件描述符
void FD_ZERO(fd_set *set);
- 清除set中所有文件描述符

缺点:

  1. 每次调用 select ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
  2. 同时每次调用 select 都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时也很大
  3. select支持的文件描述符数量太小了,默认是 1024
  4. fds集合不能重用,每次都需要重置

poll函数

1
2
3
4
5
6
7
8
9
10
11
#include <poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);
- struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
- events 输入参数,指定对文件描述符fd监听的位掩码,revents 输出参数,由内核填充,与实际发生事件联系
- timeout 指定监听文件描述符应该阻塞的毫秒数,-1则一直阻塞直到有变化
- 返回0则超时且无文件描述符有变化;大于0返回变化的文件描述符数;-1错误

解决了select的3、4缺点,但依然存在1、2缺点

epoll函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/epoll.h>

int epoll_create(int size);
- 创建一个保证能够正确处理的size大小的epoll句柄,已放弃使用
- 创建后占用一个文件描述符,将文件描述符作为返回值,失败返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 操作创建的epoll,注册要监听的事件类型
- op表示动作,EPOLL_CTL_ADD添加新的fd到epfd中;EPOLL_CTL_MOD修改已注册的fd的监听事件;EPOLL_CTL_DEL从epfd中删除一个fd
- fd 表示需要监听的文件描述符
- struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中,events监听事件有EPOLLIN对应的文件描述符可读;EPOLLOUT可写;EPOLLERR发生错误;EPOLLLET边缘触发模式(默认为水平触发模式)
- typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 等待监听事件的发生
- events 传出参数,从内核得到事件的集合
- maxevents 结构体数组events的大小
- timeout 阻塞时间,设为0不阻塞;-1一直阻塞;> 0 阻塞的时长(毫秒)
- 返回发生变化的文件描述符个数,失败返回-1

库CentOS与Ubuntu哪个更适合做服务器系统


webserver
https://kevin346-sc.github.io/2023/03/12/webserver/
作者
Kevin Huang
发布于
2023年3月12日
许可协议