Linux服务器 网络编程
1. Linux系统编程入门 GCC gcc和g++都是GCC组织的编译器 ,套件和库;一般用gcc和g++两个编译器
gcc不能自动和cpp程序库链接,一般编译和链接都用g++,因为g++会调用gcc
gcc编译c文件 gcc test.c -o app 然后./app 其中-o 指明输出文件名
预处理 编译 汇编 链接
-c 只是汇编不链接,生成目标文件“.o” test.o
-S 只是编译不汇编,生成汇编代码 test.s
-E 只进行预编译,不做其他处理 test.i
后置 :
-D 指定宏 配合#ifdef DEBUG #endif g++ test.cpp -o test -DDEBUG 指定宏DEGUB
-I 指定include包含文件搜索目录
-L 指定编译时 库 的路径
-l 指定编译时 使用的 库
库
代码仓库,保存一些变量、函数、类等;编写差不多,但不能单独运行;
静态库在程序的链接阶段被复制到程序中;动态库在程序运行时,加载到内存中供程序调用
好处:1. 代码保密(cpp反汇编还原度低);2. 方便部署和分发
工作原理 :静态库GCC链接时,把静态库代码打包到可执行程序中; 动态度GCC链接时,动态库代码不会 被打包到可执行程序中,运行时加载到内存中,通过ldd(list dynamic dependencies)命令检查动态库依赖关系;系统加载可执行代码时,需要知道库的名字和绝对路径,一般用动态载入器获取绝对路径,然后找到库文件载入内存中 优缺点比较 :
静态库被打包到应用程序 ;加载速度快; 但消耗资源,浪费内存; 更新速度慢;
动态库 可以进程资源共享(共享库);更新部署简单;控制加载时间; 但加载速度慢;发布程序需要提供依赖的动态库;
库比较小用静态 大用动态
静态库
动态库
MakeFile 一个工程中的源文件不计其数,其按类型、功能、模块分别放在若干个目录中,Makefile文件定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为Makefile文件就像一个Shell脚本一样,也可以执行操作系统的命令。
◼Makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
make是一个命令工具,是一个解释Makefile文件中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如Delphi的make,VisualC++的nmake,Linux下GNU的make。
makefile文件书写规则 1 2 3 4 5 6 7 8 9 10 11 12 13 src=$(wildcard ./*c) objs=$(patsubst %.c, %.o, $(src) ) target=app $(target) :$(objs) $(CC) $(objs) -o $(target) %.o:%.c $(CC) -c $< -o $@ .PHONY:clean clean: rm $(objs) -f
GDB 调试工具,是许多类Unix系统中的标准开发环境
主要功能:断点调试,监控,改BUG
准备工作 在生成可执行文件时添加
-O关掉优化选项
-g打开调试选项:可执行文件中加入源代码信息,第几条机器指令对应源代码几行
-Wall打开所有warning
gdb基本操作
1 2 # 应该要保证可执行文件和源文件都在 gdb 目标程序
断点操作
退出gdb后断点全部失效
调试命令
bt打印堆栈
Linux系统的IO函数 标准C库IO函数带有缓冲区,有FILE*fp文件指针,减少写磁盘次数;但是Linux系统自带的IO没有缓冲区
在网络通信时候用linux系统自带的IO;
在磁盘读写用标准C库IO;
FILE类型 文件描述符
库函数说明查找
1 2 3 4 # Linux库函数 man 2 xxx # 标准C库 man 3 xxx
open函数
打开文件 int open(const char *pathname, int flags);的说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 头文件: #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> 打开文件操作 1. 打开一个已经存在的文件 int open (const char *pathname, int flags) ; 参数: - pathname:要打开的文件路径 - flags:对文件的操作权限设置还有其他的设置 - flO_RDONLY, O_WRONLY, O_RDWR 这三个设置是互斥的 - 返回值:返回一个新的文件描述符,如果调用失败,返回-1 2. errno:属于Linux系统函数库,库里面的一个全局变量,记录的是最近的错误号。 #include <stdio.h> void perror (const char *s) ;作用:打印errno对应的错误描述 s参数:用户描述,比如hello,最终输出的内容是 hello:xxx(实际的错误描述) 创建一个新的文件 int open (const char *pathname, int flags, mode_t mode) ;
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main () { int fd = open("a.txt" ,O_RDONLY); if (fd==-1 ){ perror("open" ); } close(fd); return 0 ; }
创建文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int open (const char *pathname, int flags, mode_t mode) ;的说明参数: - pathname:要创建的文件的路径 - flags:对文件的操作权限和其他的设置 - 必选项:O_RDONLY, O_WRONLY, O_RDWR 这三个之间是互斥的,就是权限的设置只读、只写、读写 - 可选项:O_CREAT 文件不存在,创建新文件 - mode:八进制的数,表示创建出的新的文件的操作权限,比如:0775 最终的权限是:mode & ~umask 0777 -> 111111111 & 0775 -> 111111101 ---------------------------- 111111101 按位与:0 和任何数都为0 umask的作用就是抹去某些权限,也可以自己 flags参数是一个int 类型的数据,占4 个字节,32 位。 flags 32 个位,每一位就是一个标志位。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main () { int fd = open("create.txt" ,O_RDONLY|O_CREAT,0777 ); if (fd==-1 ){ perror("open" ); } close(fd); return 0 ; }
read函数 1 2 3 4 5 6 7 8 9 10 11 #include <unistd.h> ssize_t read (int fd, void *buf, size_t count) ; 参数: - fd:文件描述符,open得到的,通过这个文件描述符操作某个文件 - buf:需要读取数据存放的地方,数组的地址(传出参数) - count:指定的数组的大小 返回值: - 成功: >0 : 返回实际的读取到的字节数 =0 :文件已经读取完了 - 失败:-1 ,并且设置errno
write函数 1 2 3 4 5 6 7 8 9 ssize_t write (int fd, const void *buf, size_t count) ; 参数: - fd:文件描述符,open得到的,通过这个文件描述符操作某个文件 - buf:要往磁盘写入的数据,数据 - count:要写的数据的实际的大小 write(fd, " " , 1 ); 返回值: 成功:实际写入的字节数 失败:返回-1 ,并设置errno
例子:读写复制文件
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 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> int main () { int srcfd = open("english.txt" ,O_RDONLY); int destfd = open("copy.txt" ,O_WRONLY|O_CREAT,0664 ); if (destfd==-1 ){ perror("open" ); } char buf[1024 ]; int len=0 ; while ((len = read(srcfd,buf,sizeof (buf)))>0 ){ write(destfd,buf,len); } close(srcfd); close(destfd); return 0 ; }
lseek移动文件指针偏移 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 标准C库的函数 #include <stdio.h> int fseek (FILE *stream, long offset, int whence) ; Linux系统函数 #include <sys/types.h> #include <unistd.h> off_t lseek (int fd, off_t offset, int whence) ; 参数: - fd:文件描述符,通过open得到的,通过这个fd操作某个文件 - offset:偏移量 - whence: SEEK_SET 设置文件指针的偏移量 SEEK_CUR 设置偏移量:当前位置 + 第二个参数offset的值 SEEK_END 设置偏移量:文件大小 + 第二个参数offset的值 返回值:返回文件指针的位置 作用: 1. 移动文件指针到文件头 lseek(fd, 0 , SEEK_SET); 2. 获取当前文件指针的位置 lseek(fd, 0 , SEEK_CUR); 3. 获取文件长度 lseek(fd, 0 , SEEK_END); 4. 拓展文件的长度,当前文件10b , 110b , 增加了100 个字节 lseek(fd, 100 , SEEK_END) 注意:需要写一次数据
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 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main () { int fd = open("hello.txt" , O_RDWR); if (fd == -1 ) { perror("open" ); return -1 ; } int ret = lseek(fd, 100 , SEEK_END); if (ret == -1 ) { perror("lseek" ); return -1 ; } write(fd, " " , 1 ); close(fd); return 0 ; }
stat函数
stat函数 lstat函数获取文件信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> int stat (const char *pathname, struct stat *statbuf) ; 作用:获取一个文件相关的一些信息 参数: - pathname:操作的文件的路径 - statbuf:结构体变量,传出参数,用于保存获取到的文件的信息 返回值: 成功:返回0 失败:返回-1 设置errno int lstat (const char *pathname, struct stat *statbuf) ;获取软链接的文件信息 参数: - pathname:操作的文件的路径 - statbuf:结构体变量,传出参数,用于保存获取到的文件的信息 返回值: 成功:返回0 失败:返回-1 设置errno
例子:用stat实现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 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <pwd.h> #include <grp.h> #include <time.h> #include <string.h> int main (int argc,char * argv[]) { if (argc<2 ){ printf ("%s filename\n" ,argv[0 ]); return -1 ; } struct stat st ; int ret=stat(argv[1 ],&st); if (ret==-1 ){ perror("stat" ); return -1 ; } char perms[11 ] ={0 }; switch (st.st_mode & __S_IFMT) { case __S_IFLNK: perms[0 ]='l' ; break ; case __S_IFDIR: perms[0 ]='d' ; break ; case __S_IFREG: perms[0 ]='-' ; break ; case __S_IFBLK: perms[0 ]='b' ; break ; case __S_IFCHR: perms[0 ]='c' ; break ; case __S_IFSOCK: perms[0 ]='s' ; break ; case __S_IFIFO: perms[0 ]='p' ; break ; default : perms[0 ]='?' ; break ; } perms[1 ]= st.st_mode & S_IRUSR ? 'r' :'-' ; perms[2 ]= st.st_mode & S_IWUSR ? 'w' :'-' ; perms[3 ]= st.st_mode & S_IXUSR ? 'x' :'-' ; perms[4 ]= st.st_mode & S_IRUSR ? 'r' :'-' ; perms[5 ]= st.st_mode & S_IWGRP ? 'w' :'-' ; perms[6 ]= st.st_mode & S_IXGRP ? 'x' :'-' ; perms[7 ]= st.st_mode & S_IROTH ? 'r' :'-' ; perms[8 ]= st.st_mode & S_IWOTH ? 'w' :'-' ; perms[9 ]= st.st_mode & S_IXOTH ? 'x' :'-' ; int linkNum =st.st_nlink; char *fileUser = getpwuid(st.st_uid)->pw_name; char *fileGrp=getgrgid(st.st_uid)->gr_name; long int fileSize =st.st_size; char *time = ctime(&st.st_mtime); char mtime[512 ]={0 }; strncpy (mtime,time,strlen (time) -1 ); char buf[1024 ]; sprintf (buf,"%s %d %s %s %ld %s %s" ,perms,linkNum,fileUser,fileGrp,fileSize,mtime,argv[1 ]); printf ("%s\n" ,buf); return 0 ; }
文件属性操作函数 access判断文件权限或文件是否存在 #include <unistd.h>
int access(const char *pathname, int mode);
作用:判断某个文件是否有某个权限,或者判断文件是否存在
参数:
- pathname: 判断的文件路径
- mode:
R_OK: 判断是否有读权限
W_OK: 判断是否有写权限
X_OK: 判断是否有执行权限
F_OK: 判断文件是否存在
返回值:成功返回0, 失败返回-1
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <unistd.h> #include <stdio.h> int main () { int ret = access("a.txt" , F_OK); if (ret == -1 ) { perror("access" ); } printf ("文件存在!!!\n" ); return 0 ; }
chmod函数修改文件权限 1 2 3 4 5 6 7 #include <sys/stat.h> int chmod (const char *pathname, mode_t mode) ; 修改文件的权限 参数: - pathname: 需要修改的文件的路径 - mode:需要修改的权限值,八进制的数 返回值:成功返回0 ,失败返回-1
例子:
1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/stat.h> #include <stdio.h> int main () { int ret = chmod("a.txt" , 0777 ); if (ret == -1 ) { perror("chmod" ); return -1 ; } return 0 ;}
truncate修改文件尺寸 1 2 3 4 5 6 7 8 9 #include <unistd.h> #include <sys/types.h> int truncate (const char *path, off_t length) ; 作用:缩减或者扩展文件的尺寸至指定的大小 参数: - path: 需要修改的文件的路径 - length: 需要最终文件变成的大小 返回值: 成功返回0 , 失败返回-1
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <unistd.h> #include <sys/types.h> #include <stdio.h> int main () {int ret = truncate("b.txt" , 5 );if (ret == -1 ) { perror("truncate" ); return -1 ; } return 0 ;}
目录操作函数 mkdir函数创建目录 1 2 3 4 5 6 7 8 9 #include <sys/stat.h> #include <sys/types.h> int mkdir (const char *pathname, mode_t mode) ; 作用:创建一个目录 参数: pathname: 创建的目录的路径 mode: 权限,八进制的数 返回值: 成功返回0 , 失败返回-1
chdir修改进程工作目录 1 2 3 4 5 6 #include <unistd.h> int chdir (const char *path) ; 作用:修改进程的工作目录 比如在/home/nowcoder 启动了一个可执行程序a.out, 进程的工作目录 /home/nowcoder 参数: path : 需要修改的工作目录
getcwd获取当前目录 1 2 3 4 5 6 7 8 #include <unistd.h> char *getcwd (char *buf, size_t size) ; 作用:获取当前工作目录 参数: - buf : 存储的路径,指向的是一个数组(传出参数) - size: 数组的大小 返回值: 返回的指向的一块内存,这个数据就是第一个参数
例子
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 #include <unistd.h> #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> int main () { char buf[128 ]; getcwd(buf, sizeof (buf)); printf ("当前的工作目录是:%s\n" , buf); int ret = chdir("/home/nowcoder/Linux/lesson13" ); if (ret == -1 ) { perror("chdir" ); return -1 ; } int fd = open("chdir.txt" , O_CREAT | O_RDWR, 0664 ); if (fd == -1 ) { perror("open" ); return -1 ; } close(fd); char buf1[128 ]; getcwd(buf1, sizeof (buf1)); printf ("当前的工作目录是:%s\n" , buf1); return 0 ; }
目录遍历函数 opendir() 1 2 3 4 5 6 7 8 9 #include <sys/types.h> #include <dirent.h> DIR *opendir (const char *name) ; 参数: - name: 需要打开的目录的名称 返回值: DIR * 类型,理解为目录流 错误返回NULL
readdir() 1 2 3 4 5 6 7 #include <dirent.h> struct dirent *readdir (DIR *dirp) ; - 参数:dirp是opendir返回的结果 - 返回值: struct dirent ,代表读取到的文件的信息 读取到了末尾或者失败了,返回NULL //返回值 dirent 结构体的具体 元素 如图
closedir() 1 2 3 4 #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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <sys/types.h> #include <dirent.h> #include <stdio.h> #include <dirent.h> #include <string.h> #include <stdlib.h> int getFilenum (const char * path) ;int main (int argc, char * argv[]) { if (argc<2 ){ printf ("%s path\n" ,argv[0 ]); return -1 ; } int num=getFilenum(argv[1 ]); printf ("平台文件的个数是 %d\n" ,num); return 0 ; } int getFilenum (const char * path) { DIR *dir=opendir(path); if (dir==NULL ){ perror("opendir" ); return -1 ; } struct dirent *ptr ; int total=0 ; while ((ptr=readdir(dir))!=NULL ){ char * dname=ptr->d_name; if (strcmp (dname,"." )==0 || strcmp (dname,".." )==0 ){ continue ; } if (ptr->d_type == DT_DIR){ char newpath[256 ]; sprintf (newpath,"%s/%s" ,path,dname); total+=getFilenum(newpath); } if (ptr->d_type == DT_REG){ total++; } } closedir(dir); return total; }
文件描述符操作函数 一个进程中有一个文件描述符表
dup()文件描述符 复制 - 浅拷贝 #include <unistd.h>
int dup(int oldfd);
作用:复制一个新的文件描述符
fd=3, int fd1 = dup(fd),
fd指向的是a.txt, fd1也是指向a.txt
从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
例子:
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 #include <unistd.h> #include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> int main () { int fd = open("a.txt" , O_RDWR | O_CREAT, 0664 ); int fd1 = dup(fd); if (fd1 == -1 ) { perror("dup" ); return -1 ; } printf ("fd : %d , fd1 : %d\n" , fd, fd1); close(fd); char * str = "hello,world" ; int ret = write(fd1, str, strlen (str)); if (ret == -1 ) { perror("write" ); return -1 ; } close(fd1); return 0 ; }
dup2()文件描述符 重定向 1 2 3 4 5 6 7 #include <unistd.h> int dup2 (int oldfd, int newfd) ; 作用:重定向文件描述符,让newfd也来指向oldfd oldfd 指向 a.txt, newfd 指向 b.txt 调用函数成功后:newfd 和 b.txt 做close, newfd 指向了 a.txt oldfd 必须是一个有效的文件描述符 oldfd和newfd值相同,相当于什么都没有做
例子:
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 #include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> int main () { int fd = open("1.txt" , O_RDWR | O_CREAT, 0664 ); if (fd == -1 ) { perror("open" ); return -1 ; } int fd1 = open("2.txt" , O_RDWR | O_CREAT, 0664 ); if (fd1 == -1 ) { perror("open" ); return -1 ; } printf ("fd : %d, fd1 : %d\n" , fd, fd1); int fd2 = dup2(fd, fd1); if (fd2 == -1 ) { perror("dup2" ); return -1 ; } char * str = "hello, dup2" ; int len = write(fd1, str, strlen (str)); if (len == -1 ) { perror("write" ); return -1 ; } printf ("fd : %d, fd1 : %d, fd2 : %d\n" , fd, fd1, fd2); close(fd); close(fd1); return 0 ; }
fcntl()函数 控制文件描述符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <unistd.h> #include <fcntl.h> int fcntl (int fd, int cmd, ...) ;参数: fd : 表示需要操作的文件描述符 cmd: 表示对文件描述符进行如何操作 - F_DUPFD : 复制文件描述符,复制的是第一个参数fd,得到一个新的文件描述符(返回值) int ret = fcntl(fd, F_DUPFD); - F_GETFL : 获取指定的文件描述符文件状态flag 获取的flag和我们通过open函数传递的flag是一个东西。 - F_SETFL : 设置文件描述符文件状态flag 必选项:O_RDONLY, O_WRONLY, O_RDWR 不可以被修改!!!!! 可选性:O_APPEND, O_NONBLOCK O_APPEND 表示追加数据 NONBLOK 设置成非阻塞 阻塞和非阻塞:描述的是函数调用的行为。 导致当前进程线程被挂起-阻塞
例子
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 #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <string.h> int main () { int fd = open("1.txt" , O_RDWR); if (fd == -1 ) { perror("open" ); return -1 ; } int flag = fcntl(fd, F_GETFL); if (flag == -1 ) { perror("fcntl" ); return -1 ; } flag |= O_APPEND; int ret = fcntl(fd, F_SETFL, flag); if (ret == -1 ) { perror("fcntl" ); return -1 ; } char * str = "nihao" ; write(fd, str, strlen (str)); close(fd); return 0 ; }
2. Linux多进程编程 2.1进程状态转换 -终端指令
Linux中PBC的结构体task_struct
进程参数查看 ulimit -a
查看进程 ps aux / ajx a:显示终端上的所有进程,包括其他用户的进程 u:显示进程的详细信息 x:显示没有控制终端的进程 j:列出与作业控制相关的信息
STAT参数意义: D 不可中断 Uninterruptible(usually IO) R 正在运行,或在队列中的进程 S(大写) 处于休眠状态 T 停止或被追踪 Z 僵尸进程 W 进入内存交换(从内核2.6开始无效) X 死掉的进程 < 高优先级 N 低优先级 s 包含子进程
+ 位于前台的进程组
然后,实时显示进程动态 top 可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令 执行后,可以按以下按键对显示的结果进行排序: ⚫ M 根据内存使用量排序 ⚫ P 根据 CPU 占有率排序 ⚫ T 根据进程运行时间长短排序 ⚫ U 根据用户名来筛选进程 ⚫ K 输入指定的 PID 杀死进程
杀死进程 kill [-signal] pid kill –l 列出所有信号 kill –SIGKILL 进程ID kill -9 进程ID killall name 根据进程名杀死进程
进程号和进程组相关函数: ⚫ pid_t getpid(void); ⚫ pid_t getppid(void); //父进程的pid ⚫ pid_t getpgid(pid_t pid);
2.2进程创建 fork()函数
创建子进程
#include <sys/types.h> #include <unistd.h>
pid_t fork(void); 函数的作用:用于创建子进程。 返回值: fork()的返回值会返回两次。一次是在父进程中,一次是在子进程中。 在父进程中返回创建的子进程的ID, 在子进程中返回0 如何区分父进程和子进程:通过fork的返回值。 在父进程中返回-1,表示创建子进程失败,并且设置errno 父子进程之间的关系: 区别: 1.fork()函数的返回值不同 父进程中: >0 返回的子进程的ID 子进程中: =0 2.pcb中的一些数据 当前的进程的id pid 当前的进程的父进程的id ppid 信号集 共同点: 某些状态下:子进程刚被创建出来,还没有执行任何的写数据的操作 - 用户区的数据 - 文件描述符表 父子进程对变量是不是共享的? - 刚开始的时候,是一样的,共享的。如果修改了数据,不共享了。 - 读时共享(子进程被创建,两个进程没有做任何的写的操作),写时拷贝。 实际上,更准确来说,Linux 的 fork() 使用是通过写时拷贝 (copy- on-write) 实现。 写时拷贝是一种可以推迟甚至避免拷贝数据的技术。 内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。 只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。 也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。 注意:fork之后父子进程共享文件, fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
查看例子
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 #include <sys/types.h> #include <unistd.h> #include <string.h> #include <stdio.h> int main () { int num=10 ; pid_t pid=fork(); if (pid >0 ){ printf ("pid: %d" , pid); printf ("parent process,pid:%d,ppid:%d\n" ,getpid(),getppid()); printf ("parent num:%d\n" ,num); num+=10 ; printf ("parent num +=10:%d\n" ,num); }else if (pid==0 ){ printf ("child process,pid:%d,ppid:%d" ,getpid(),getppid()); printf ("child num:%d\n" ,num); num+=100 ; printf ("child num +=10:%d\n" ,num); } for (int i=0 ;i<3 ;i++){ printf ("i:%d,pid:%d\n" ,i,getpid()); sleep(1 ); } return 0 ; }
父进程和子进程执行的代码块分别是以下图:
子进程只执行以下的自己进程部分
父子进程代码段一样,但是按照pid来运行;
写操作之后,进程资源之间互不影响,共同部分运行的是各自的资源
gdb调试父子进程 使用 GDB 调试的时候,GDB 默认只能跟踪一个进程,可以在 fork 函数调用之前,通过指令设置 GDB 调试工具跟踪父进程或者是跟踪子进程,默认跟踪父进程。
设置调试父进程或者子进程:set follow-fork-mode [parent(默认)| child]
设置调试模式:set detach-on-fork [on | off] 默认为 on,表示调试当前进程的时候,其它的进程继续运行,如果为 off,调试当前进程的时候,其它进程被 GDB 挂起。
查看调试的进程:info inferiors 切换当前调试的进程为id进程:inferior id 使进程脱离 GDB 调试:detach inferiors id
exec函数族 ◼ exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
◼ exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。
注意:
内核区的id等不会改变,但是用户取的数据都被替换
有p或者v后缀,一般filename直接写文件名,因为p会指定到环境变量查,v指定到数组envp[]中路径依次查找,没找到返回-1
一般fork新进程后,替换子进程数据
1 2 3 4 5 6 7 8 9 10 11 12 13 exec 函数族 ◼ 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) 存有环境变量字符串地址的指针数组的地址
execl()
#include <unistd.h>
int execl(const char *path, const char *arg, …); - 参数: - path:需要指定的执行的文件的路径或者名称 a.out /home/nowcoder/a.out 推荐使用绝对路径 ./a.out hello world - arg:是执行可执行文件所需要的参数列表 第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称 从第二个参数开始往后,就是程序执行所需要的的参数列表。 参数最后需要以NULL结束(哨兵)
返回值: 只有当调用失败,才会有返回值,返回-1,并且设置errno 如果调用成功,没有返回值。
execlp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <unistd.h> int execlp (const char *file, const char *arg, ... ) ; - 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。 - 参数: - file:需要执行的可执行文件的文件名 a.out ps - arg:是执行可执行文件所需要的参数列表 第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称 从第二个参数开始往后,就是程序执行所需要的的参数列表。 参数最后需要以NULL 结束(哨兵) - 返回值: 只有当调用失败,才会有返回值,返回-1 ,并且设置errno 如果调用成功,没有返回值。
execv
1 2 3 4 5 6 7 int execv (const char *path, char *const argv[]) ;argv是需要的参数的一个字符串数组 char * argv[] = {"ps" , "aux" , NULL };execv("/bin/ps" , argv); int execve (const char *filename, char *const argv[], char *const envp[]) ;char * envp[] = {"/home/nowcoder" , "/home/bbb" , "/home/aaa" };
2.3 进程退出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main () { printf ("hello\n" ); printf ("world" ); _exit(0 ); return 0 ; }
孤儿进程 ◼ 父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。 ◼ 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束 了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。 ◼ 因此孤儿进程并不会有什么危害
僵尸进程 ◼ 每个进程结束之后, 都会释放自己地址空间中的用户区数据,内核区的 PCB 没有办法自己释放掉,需要父进程 去释放。 ◼ 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。 ◼ 僵尸进程不能被 kill -9 杀死,这样就会导致一个问题,如果父进程不调用 wait()或 waitpid() 的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
进程回收 ◼ 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。 ◼ 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
wait()函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
//wstatus指针传入到宏里面可得到 退出的具体信息
//如果不需要查看直接 wait(NULL)
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.
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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 ◼ wait() 和 waitpid() 函数的功能一样,区别在于: **wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。** ◼ 注意:一次wait或waitpid调用**只能清理一个子进程**,清理多个子进程应使用循环:while() * 退出信息相关宏函数查询 * ~~~c ◼ WIFEXITED(status) 非0,进程正常退出 ◼ WEXITSTATUS(status) 如果上宏为真,获取进程退出的状态(exit的参数) ◼ WIFSIGNALED(status) 非0,进程异常终止 ◼ WTERMSIG(status) 如果上宏为真,获取使进程终止的信号编号 ◼ WIFSTOPPED(status) 非0,进程处于暂停状态 ◼ WSTOPSIG(status) 如果上宏为真,获取使进程暂停的信号的编号 ◼ WIFCONTINUED(status) 非0,进程暂停后已经继续运行 ~~~ 例子; * ~~~c #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(){ pid_t pid; //创建5个子进程 for(int i=0;i!=5;++i){ pid = fork(); if(pid == 0){ break; } } if(pid > 0){ //父进程 while(1){ printf("父进程: %d\n",getpid()); int st; int ret = wait(&st); if(ret == -1){ break; } if(WIFEXITED(st)){ //是不是正常退出 printf("退出状态码: %d\n",WEXITSTATUS(st)); } if(WIFSIGNALED(st)){ //是不是被异常终止 printf("被哪个信号干掉: %d\n",WEXITSTATUS(st)); } printf("child die,pid = %d\n",WTERMSIG(st)); sleep(1); } }else if(pid == 0){ //子进程 printf("子进程: %d\n",getpid()); exit(0); sleep(1); } return 0; } ~~~ ###### waitpid()函数 ~~~c #include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *wstatus, int options); 功能:回收指定进程号的子进程,可以设置是否阻塞。 参数: - pid: pid > 0 : 某个子进程的pid pid = 0 : 回收当前进程组的所有子进程 pid = -1 : 回收任意的子进程,相当于 wait() (最常用) pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程 - 参数:int *wstatus 进程退出时的状态信息,传入的是一个int类型的地址,传出参数。 - options:设置阻塞或者非阻塞 0 : 阻塞 WNOHANG : 非阻塞 - 返回值: //这三个会返回值可以判断当前进程的子进程回收情况,可以以此来设计,很多子进程时,回收处理函数 > 0 : 返回子进程的id = 0 : options=WNOHANG, 表示还有子进程活着 = -1 :错误,或者没有子进程了 ~~~ * 例子(设置非阻塞): * ~~~c #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(){ pid_t pid; //创建5个子进程 for(int i=0;i!=5;++i){ pid = fork(); if(pid == 0){ break; } } if(pid > 0){ //父进程 while(1){ printf("父进程: %d\n",getpid()); sleep(1); int st; //int ret = waitpid(-1,&st,0); int ret = waitpid(-1,&st,WNOHANG); if(ret == -1){ break; }else if(ret == 0){ //子进程还有在运行的 continue; }else if(ret > 0){ if(WIFEXITED(st)){ printf("退出的状态码:%d\n",WEXITSTATUS(st)); } if(WIFSIGNALED(st)){ printf("被哪个信号干掉:%d",WTERMSIG(st)); } } printf("child die,pid = %d\n",WTERMSIG(st)); sleep(1); } }else if(pid == 0){ //子进程 while (1) { printf("子进程: %d\n",getpid()); sleep(1); } exit(0); } return 0; } ~~~ ### 2.4进程间通信IPC 管道在使用完毕后应该被关闭,这是为了防止出现资源泄漏的情况。如果没有关闭管道,在程序运行过程中会一直占用系统资源,而且可能导致其他进程无法使用同名管道。同时,如果管道没有被及时关闭,在程序意外退出时也可能导致数据丢失或者磁盘空间占用问题。因此,在使用完毕后,最好及时关闭管道。 * TCP/IP方式: mysql -h127.0.0.1 -uroot -P3306 -p * windowOS 的一台主机:命名管道和共享内存 * 类Unix的一台主机:套接字 * (匿名)管道,Unix系统最古老的通信方式,所有Unix系统都支持 * 统计目录中文件数量的命令: ls | wc -l 实际上就是创建管道 把写入端导出读取端 ##### 管道特点 1. 管道其实是内核在内存中维护的缓冲区,缓冲能力有限,不同操作系统大小不同; 2. 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但是不存储数据,可以按照操作文件的方式操作管道。 3. 一个管道是一个字节流,使用管道不存在消息和消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块是多少(每次都按照自己能力读取) 4. 通过管道传递是顺序的。 5. 管道中传递数据方向是单向的,一端写入,一端读取,也就是半双工。 6. 从管道中读取数据是一次性操作,数据一旦被读走了,就从管道抛弃了,(队列实现)释放空间以便写入更多的数据,在管道中无法使用lseek()随机访问数据 7. 管道只能在具有公共祖先的进程(父子进程,兄弟进程,具有亲缘关系)之间使用 * 管道原理:子进程forkc()出来之后,可以共享文件描述符,可以操作建立管道 * 管道数据结构:环形队列 ##### 匿名管道传输pipe()函数 ~~~c #include <unistd.h> int pipe(int pipefd[2]); 功能:创建一个匿名管道,用来进程间通信。 参数:int pipefd[2] 这个数组是一个传出参数。 pipefd[0] 对应的是管道的读端 pipefd[1] 对应的是管道的写端 返回值: 成功 0 失败 -1 管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞 注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程) ~~~ * 例子-父子间管道通信 * 匿名管道一般不会实现双向读写,因为没有了sleep(),本进程会抢读管道,一般就是一个读,一个写,并且关闭另一个操作,俺理解为互斥 ~~~c //在fork()之前发送数据给父进程,父进程读取到数据输出 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(){ //在fork()之前创建管道 int pipefd[2]; int ret=pipe(pipefd); if(ret==-1){ perror("pipe"); exit(0); } //创建子进程 pid_t pid = fork(); if(pid>0){ //父进程先读取,然后写 printf("父进程"); char buf[1024]={0}; while(1){ //先读 //sizeof给出操作数的存储地址空间 int len = read(pipefd[0],buf,sizeof(buf)); printf("parent recv: %s ,pid : %d\n",buf,getpid()); //向管道写入数据 char* str="hello,this is parent process"; write(pipefd[1],str,strlen(str)); sleep(1); } }else if(pid ==0){ //子进程先写,然后读 //管道默认阻塞,sleep(10); 如果没有数据,读取端就等着 char buf[1024]={0}; printf("子进程"); while(1){ //先写 char* str="hello,this is child process"; //strlen()函数给出字符串的长度 write(pipefd[1],str,strlen(str)); sleep(1); //读取数据 int len = read(pipefd[0],buf,sizeof(buf)); printf("child recv: %s ,pid : %d\n",buf,getpid()); //清除一下 bzero(buff,1024); } } return 0; } ~~~ * 互斥读写操作的代码如下 * ~~~c //在fork()之前发送数据给父进程,父进程读取到数据输出 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(){ //在fork()之前创建管道 int pipefd[2]; int ret=pipe(pipefd); if(ret==-1){ perror("pipe"); exit(0); } //创建子进程 pid_t pid = fork(); if(pid>0){ //父进程读取 printf("父进程\n"); //关闭写端 close(pipefd[1]); char buf[1024]={0}; while(1){ int len = read(pipefd[0],buf,sizeof(buf)); printf("parent recv: %s ,pid : %d\n",buf,getpid()); } }else if(pid ==0){ //子进程写端 printf("子进程\n"); //关闭读端 close(pipefd[0]); while(1){ char* str="hello,this is child process"; //strlen()函数给出字符串的长度 write(pipefd[1],str,strlen(str)); sleep(1); } } return 0; } ~~~ * 获取管道大小 * // 1. 函数获取管道的大小 long size = fpathconf(pipefd[0], _PC_PIPE_BUF); // 2. 终端获取管道信息 ulimit -a ##### ps aux | grep xxx 操作实现 -父子进程通信案例 ~~~c //在fork()之前发送数据给父进程,父进程读取到数据输出 #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <wait.h> int main(){ //创建管道 int fd[2]; int ret=pipe(fd); if(ret==-1){ perror("pipe"); exit(0); } //创建子进程 pid_t pid = fork(); if(pid > 0){ //父进程,读取管道信息输出到列表 close(fd[1]); char buf[1024]={0}; int len=-1; //空间设置 -1 留一个字符串的结束符位置 while((len=read(fd[0],buf,sizeof(buf)-1))>0){ //过滤数据输出 printf("%s",buf); //清空buf内容 memset(buf,0,1024); } wait(NULL); }else if(pid == 0){ //子进程,把输出stdout_fileno的内容写入管道 close(fd[0]); // 文件描述符的重定向 stdout_fileno -> fd[1] dup2(fd[1],STDOUT_FILENO); //替换到aux的内容 execlp("ps","ps","aux",NULL); //如果没退出,报系统错误 perror("execlp"); exit(0); } return 0; } ~~~ ##### 管道读写的特点 ~~~c 管道的读写特点: 使用管道时,需要注意以下几种特殊的情况(假设都是阻塞I/O操作) 1.所有的指向管道写端的文件描述符都关闭了(管道写端引用计数为0),有进程从管道的读端 读数据,那么管道中剩余的数据被读取以后,再次read会返回0,就像读到文件末尾一样。 2.如果有指向管道写端的文件描述符没有关闭(管道的写端引用计数大于0),而持有管道写端的进程 也没有往管道中写数据,这个时候有进程从管道中读取数据,那么管道中剩余的数据被读取后, 再次read会阻塞,直到管道中有数据可以读了才读取数据并返回。 3.如果所有指向管道读端的文件描述符都关闭了(管道的读端引用计数为0),这个时候有进程 向管道中写数据,那么该进程会收到一个信号SIGPIPE, 通常会导致进程异常终止。 4.如果有指向管道读端的文件描述符没有关闭(管道的读端引用计数大于0),而持有管道读端的进程 也没有从管道中读数据,这时有进程向管道中写数据,那么在管道被写满的时候再次write会阻塞, 直到管道中有空位置才能再次写入数据并返回。 总结: 读管道: 管道中有数据,read返回实际读到的字节数。 管道中无数据: 写端被全部关闭,read返回0(相当于读到文件的末尾) 写端没有完全关闭,read阻塞等待 写管道: 管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号) 管道读端没有全部关闭: 管道已满,write阻塞 管道没有满,write将数据写入,并返回实际写入的字节数 ~~~ * 设置非阻塞 - 此时设置为管道读端继续read,由于没有写入,read()返回-1 ~~~c int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag flags |= O_NONBLOCK; // 修改flag的值 fcntl(pipefd[0], F_SETFL, flags); // 设置新的flag ~~~ #### 有名管道 ◼ 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提 出了有名管道(FIFO),也叫命名管道、FIFO文件。 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样 即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此 通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。 ◼ 一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O系统调用了(如read()、write()和close())。与管道一样,FIFO 也有一 个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的 名称也由此而来:先入先出。 ##### 与匿名管道区别: 有名管道(FIFO)和匿名管道(pipe)有一些特点是相同的,不一样的地方在于: 1. FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。 2. 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。 3. FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。 2. 通过命令创建有名管道 mkfifo 名字 3. 通过函数创建有名管道 int mkfifo(const char *pathname, mode_t mode); ~~~c 创建fifo文件 1.通过命令: mkfifo 名字 2.通过函数:int mkfifo(const char *pathname, mode_t mode); #include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); 参数: - pathname: 管道名称的路径 - mode: 文件的权限 和 open 的 mode 是一样的 是一个八进制的数 返回值:成功返回0,失败返回-1,并设置错误号 ~~~ 4. 一旦使用 mkfifo 创建了一个 FIFO,就可以使用 **open** 打开它,常见的文件 I/O 函数都可用于 fifo。如:close、read、write、unlink 等。 5. FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是 从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。 ##### 有名管道注意事项 ~~~c 有名管道的注意事项: 1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道 2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道 读管道: 管道中有数据,read返回实际读到的字节数 管道中无数据: 管道写端被全部关闭,read返回0,(相当于读到文件末尾) 写端没有全部被关闭,read阻塞等待 写管道: 管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号) 管道读端没有全部关闭: 管道已经满了,write会阻塞 管道没有满,write将数据写入,并返回实际写入的字节数。 ~~~ ##### 同一路径下 有名管道创建并用于读写的例子 ~~~c write.c #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <unistd.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main() { // 1.判断文件是否存在 int ret = access("fifo1", F_OK); if(ret == -1) { printf("管道不存在,创建管道\n"); //2.创建管道文件 ret = mkfifo("fifo1", 0664); if(ret == -1) { perror("mkfifo"); exit(0); } } //以只写的方式打开管道 int fd=open("fifo1",O_WRONLY); if(fd==-1){ perror("open"); exit(0); } //写入数据 for (int i = 0; i < 100; i++) { char buf[1024]; //sprintf将字符串写入buf中 sprintf(buf,"hello,%d\n",i); printf("write data: %s\n",buf); write(fd,buf,strlen(buf)); sleep(1); } close(fd); return 0; } ~~~ ```c read.c #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <unistd.h> #include <unistd.h> #include <fcntl.h> int main(){ //1. 打开管道文件 int fd = open("fifo1",O_RDONLY); if(fd == -1){ perror("open"); exit(0); } //读数据 while(1){ char buf[1024] = {0}; int len = read(fd,buf,sizeof(buf)); if(len == 0){ printf("写端断开连接了。\n"); break; } printf("recv buf :%s\n",buf); } close(fd); return 0; }
2.5 内存映射 Memory-mapped 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 26 27 28 29 30 31 #include <sys/mman.h> void *mmap (void *addr, size_t length, int prot, int flags,int fd, off_t offset) ; - 功能:将一个文件或者设备的数据映射到内存中 - 参数: - void *addr: NULL , 由内核指定 - length : 要映射的数据的长度,这个值不能为0 。建议使用文件的长度。 获取文件的长度:stat lseek - prot : 对申请的内存映射区的操作权限 -PROT_EXEC :可执行的权限 -PROT_READ :读权限 -PROT_WRITE :写权限 -PROT_NONE :没有权限 要操作映射内存,必须要有读的权限。 PROT_READ、PROT_READ|PROT_WRITE - flags : - MAP_SHARED : - MAP_PRIVATE : - fd: 需要映射的那个文件的文件描述符 - 通过open得到,open的是一个磁盘文件 - 注意:文件的大小不能为0 ,open指定的权限不能和prot参数有冲突,也就是不小于需要的权限。 prot: PROT_READ open:只读/读写 prot: PROT_READ | PROT_WRITE open:读写 - offset:偏移量,一般不用。必须指定的是4 k的整数倍,0 表示不偏移。 - 返回值:返回创建的内存的首地址 失败返回MAP_FAILED,(void *) -1 int munmap (void *addr, size_t length) ; - 功能:释放内存映射 - 参数: - addr : 要释放的内存的首地址 - length : 要释放的内存的大小,要和mmap函数中的length参数的值一样
使用内存映射实现进程间通信: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 使用内存映射实现进程间通信: 1.有关系的进程(父子进程) - 还没有子进程的时候 - 通过唯一的父进程,先创建内存映射区 - 有了内存映射区以后,创建子进程 - 父子进程共享创建的内存映射区(类似于匿名管道) 2.没有关系的进程间通信 - 准备一个大小不是0的磁盘文件 - 进程1 通过磁盘文件创建内存映射区 - 得到一个操作这块内存的指针 - 进程2 通过磁盘文件创建内存映射区 - 得到一个操作这块内存的指针 - 使用内存映射区通信(也就是映射同一块文件) 注意:内存映射区通信,是非阻塞。
关系类进程通信 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 <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <wait.h> #include <string.h> #include <unistd.h> int main () { int fd = open("a.txt" ,O_RDWR); if (fd == -1 ){ perror("open" ); exit (0 ); } int size = lseek(fd,0 ,SEEK_END); void *ptr = mmap(NULL ,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (ptr == MAP_FAILED){ perror("mmap" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ){ wait(NULL ); char buf[64 ]; strcpy (buf,(char *)ptr); printf ("read data: %s" ,buf); }else if (pid == 0 ){ strcpy ((char *)ptr,"我是儿子,你好" ); } munmap(ptr,size); 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 write.c #include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <wait.h> #include <string.h> #include <unistd.h> int main () { int fd = open("a.txt" ,O_RDWR); if (fd == -1 ){ perror("open" ); exit (0 ); } int size = lseek(fd,0 ,SEEK_END); void *ptr = mmap(NULL ,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (ptr == MAP_FAILED){ perror("mmap" ); exit (0 ); } strcpy ((char *)ptr,"好哥们 你好\n" ); munmap(ptr,size); 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 read.c #include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <wait.h> #include <string.h> #include <unistd.h> int main () { int fd = open("a.txt" ,O_RDWR); if (fd == -1 ){ perror("open" ); exit (0 ); } int size = lseek(fd,0 ,SEEK_END); void *ptr = mmap(NULL ,size,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 ); if (ptr == MAP_FAILED){ perror("mmap" ); exit (0 ); } char buf[1024 ]; strcpy (buf,(char *)ptr); printf ("read data: %s" ,buf); munmap(ptr,size); return 0 ; }
内存映射注意事项
如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功? void * ptr = mmap(…); ptr++; 可以对其进行++操作 munmap(ptr, len); // 错误,要保存地址
如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样? 错误,返回MAP_FAILED open()函数中的权限建议和prot参数的权限保持一致。
如果文件偏移量为1000会怎样? 偏移量必须是4K的整数倍,返回MAP_FAILED
mmap什么情况下会调用失败? - 第二个参数:length = 0 - 第三个参数:prot - 只指定了写权限 - prot PROT_READ | PROT_WRITE 第5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY
可以open的时候O_CREAT一个新文件来创建映射区吗? - 可以的,但是创建的文件的大小如果为0的话,肯定不行 - 可以对新的文件进行扩展 - lseek() - truncate()
mmap后关闭文件描述符,对mmap映射有没有影响? int fd = open(“XXX”); mmap(,,,,fd,0); close(fd); 映射区还存在,创建映射区的fd被关闭,没有任何影响。
对ptr越界操作会怎样? void * ptr = mmap(NULL, 100,,,,,); 4K 越界操作操作的是非法的内存 -> 段错误
void *memcpy(void *dest, const void *src, size_t n);
使用内存映射实现文件的拷贝
使用内存映射实现文件拷贝的功能 思路: 1.对原始的文件进行内存映射 2.创建一个新文件(拓展该文件) 3.把新文件的数据映射到内存中 4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中 5.释放资源
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 #include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main () { int fd = open("english.txt" , O_RDWR); if (fd == -1 ) { perror("open" ); exit (0 ); } int len = lseek(fd, 0 , SEEK_END); int fd1 = open("cpy.txt" , O_RDWR | O_CREAT, 0664 ); if (fd1 == -1 ) { perror("open" ); exit (0 ); } truncate("cpy.txt" , len); write(fd1, " " , 1 ); void * ptr = mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ); void * ptr1 = mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0 ); if (ptr == MAP_FAILED) { perror("mmap" ); exit (0 ); } if (ptr1 == MAP_FAILED) { perror("mmap" ); exit (0 ); } memcpy (ptr1, ptr, len); munmap(ptr1, len); munmap(ptr, len); close(fd1); close(fd); 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 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 <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/wait.h> int main () { int len = 4096 ; void * ptr = mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1 , 0 ); if (ptr == MAP_FAILED) { perror("mmap" ); exit (0 ); } pid_t pid = fork(); if (pid > 0 ) { strcpy ((char *) ptr, "hello, world" ); wait(NULL ); }else if (pid == 0 ) { sleep(1 ); printf ("%s\n" , (char *)ptr); } int ret = munmap(ptr, len); if (ret == -1 ) { perror("munmap" ); exit (0 ); } return 0 ; }
2.6 信号 概述
概述 : 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进程发送一个中断信号。
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给 相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的 内存区域。
系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
运行 kill 命令或调用 kill 函数。
使用信号的两个主要目的是:
让进程知道已经发生了一个特定的事情。
强迫进程执行 它自己代码中的信号处理程序。
信号的特点:
简单
不能携带大量信息
满足某个特定条件才发送
优先级比较高
1. 查看系统定义的信号列表:kill –l
前 31 个信号为常规信号,其余为实时信号
2. ◼ 查看信号的详细信息:man 7 signal
◼ 信号的 5 中默认处理动作
Term 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件(Core文件包含了一些关于异常的信息)
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
3. 信号的几种状态:产生、未决、递达
4. SIGKILL 和 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 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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 * 这里注意kill 9不能杀死僵尸进程 * 可以用kill -l查看信号对应的编号 ##### kill() raise() abort() ```c #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); - 功能:给任何的进程或者进程组pid, 发送任何的信号 sig - 参数: - pid : > 0 : 将信号发送给指定的进程 = 0 : 将信号发送给当前的进程组 = -1 : 将信号发送给每一个有权限接收这个信号的进程 < -1 : 这个pid=某个进程组的ID取反 (-12345) - sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号 kill(getppid(), 9); kill(getpid(), 9); int raise(int sig); - 功能:给当前进程或者线程发送信号 - 参数: - sig : 要发送的信号 - 返回值: - 成功 0 - 失败 非0 kill(getpid(), sig); void abort(void); - 功能: 发送SIGABRT信号给当前的进程,杀死当前进程 类似于: kill(getpid(), SIGABRT); ``` 例子: ```c #include <stdio.h> #include <sys/types.h> #include <signal.h> #include <unistd.h> int main() { pid_t pid = fork(); if(pid == 0) { // 子进程 int i = 0; for(i = 0; i < 5; i++) { printf("child process\n"); sleep(1); } } else if(pid > 0) { // 父进程 printf("parent process\n"); sleep(2); printf("kill child process now\n");//这里传的是子进程pid kill(pid, SIGINT); } return 0; } ``` ##### alarm() ```c #include <unistd.h> unsigned int alarm(unsigned int seconds); - 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候, 函数会给当前的进程发送一个信号:SIGALARM,(查表,会终止进程) - 参数: seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。 取消一个定时器,通过alarm(0)。 - 返回值: - 之前没有定时器,返回0 - 之前有定时器,返回之前的定时器剩余的时间 - SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。 alarm(10); -> 返回0 过了1秒 alarm(5); -> 返回9 alarm(100) -> 该函数是不阻塞的 ``` * 例子 ```c #include <stdio.h> #include <unistd.h> //查看1秒计算机能数多少个数? /* 实际时间 = 内核时间 + 用户时间 + 消耗时间 进行文件的IO操作比较消耗时间 定时器,与进程的状态无关,alarm都会计时 */ int main(){ alarm(1); int i=0; while(1){ printf("%d\n",i++); } return 0; } ``` ##### setitimer() ```c #include <sys/time.h> int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); - 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时 - 参数: - which : 定时器以什么时间计时 ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM 常用 ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF - new_value: 设置定时器的属性 struct itimerval { // 定时器的结构体 struct timeval it_interval; // 每个阶段的时间,间隔时间 struct timeval it_value; // 延迟多长时间执行定时器 }; struct timeval { // 时间的结构体 time_t tv_sec; // 秒数 suseconds_t tv_usec; // 微秒 }; 过10秒后,每个2秒定时一次 - old_value :记录上一次的定时的时间参数,一般不使用,指定NULL - 返回值: 成功 0 失败 -1 并设置错误号 ``` ##### signal 捕捉函数 ```c #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); - 功能:设置某个信号的捕捉行为 - 参数: - signum: 要捕捉的信号 - handler: 捕捉到信号要如何处理 - SIG_IGN : 忽略信号 - SIG_DFL : 使用信号默认的行为 - 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。 回调函数: - 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义 - 不是程序员调用,而是当信号产生,由内核调用 - 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。 - 返回值: 成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL 失败,返回SIG_ERR,设置错误号 SIGKILL SIGSTOP不能被捕捉,不能被忽略。 ``` * 例子: 用setitimer配合signal捕捉 ```c #include <stdio.h> #include <sys/time.h> #include <stdlib.h> #include <signal.h> //设置回调函数 void myAlarm(int num){ //这里可以查表得到信号14是SIGALRM printf("捕捉到了信号编号是: %d\n",num); printf("xxxxxxxxxxxx\n"); } int main(){ // 注册信号捕捉 // signal(SIGALRM, SIG_IGN); // signal(SIGALRM, SIG_DFL); // void (*sighandler_t)(int); 函数指针,int类型的参数表示捕捉到的信号的值。 signal(SIGALRM,myAlarm); //设置间隔时间 struct itimerval new_value; //时间变量必须要初始化 new_value.it_interval.tv_sec = 2; new_value.it_interval.tv_usec = 0; //延迟时间,到3s后开始计时 new_value.it_value.tv_sec = 3; new_value.it_value.tv_usec = 0; //是非阻塞的 int ret = setitimer(ITIMER_REAL,&new_value,NULL); printf("开始计时\n"); if(ret == -1){ perror("setitimer"); exit(0); } //保存程序不关闭 阻塞 getchar(); return 0; } ``` ### 2.7信号集 * 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。 * 在 PCB 中有两个非常重要的信号集。一个称之为 “**阻塞信号集**”(**信号掩码(也就是阻塞信号集)**可以设置,比如empty和add) ,另一个称之为“**未决信号集**” (只读)。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。 * 信号的 “未决” 是一种**状态**,指的是从**信号的产生到信号被处理前**的这一段时间。 * 信号的 “阻塞” 是一个**开关动作**,指的是**阻止信号被处理**,但不是阻止信号产生。 * 信号的**阻塞就是让系统暂时保留信号留待以后发送**。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。 * 下图,进程的虚拟地址空间,包括用户区和内核区,内核区中有PCB进程控制块,PCB中有未决信号集和阻塞信号集 ~~~shell 1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建) 2.信号产生但是没有被处理 (未决) - 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集) - SIGINT信号状态被存储在第二个标志位上 - 这个标志位的值为0, 说明信号不是未决状态 - 这个标志位的值为1, 说明信号处于未决状态 3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较 - 阻塞信号集默认不阻塞任何的信号 - 如果想要阻塞某些信号需要用户调用系统的API 4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了 - 如果没有阻塞,这个信号就被处理 - 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理
信号集函数 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 以下信号集相关的函数都是对 自定义的信号集!! 进行操作。 int sigemptyset (sigset_t *set ) ; - 功能:清空信号集中的数据,将信号集中的所有的标志位置为0 - 参数:set ,传出参数,需要操作的信号集 - 返回值:成功返回0 , 失败返回-1 int sigfillset (sigset_t *set ) ; - 功能:将信号集中的所有的标志位置为1 - 参数:set ,传出参数,需要操作的信号集 - 返回值:成功返回0 , 失败返回-1 int sigaddset (sigset_t *set , int signum) ; - 功能:设置信号集中的某一个信号对应的标志位为1 ,表示阻塞这个信号 - 参数: - set :传出参数,需要操作的信号集 - signum:需要设置阻塞的那个信号 - 返回值:成功返回0 , 失败返回-1 int sigdelset (sigset_t *set , int signum) ; - 功能:设置信号集中的某一个信号对应的标志位为0 ,表示不阻塞这个信号 - 参数: - set :传出参数,需要操作的信号集 - signum:需要设置不阻塞的那个信号 - 返回值:成功返回0 , 失败返回-1 int sigismember (const sigset_t *set , int signum) ; - 功能:判断某个信号是否阻塞 - 参数: - set :需要操作的信号集 - signum:需要判断的那个信号 - 返回值: 1 : signum被阻塞 0 : signum不阻塞 -1 : 失败
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 #include <signal.h> #include <stdio.h> int main () { sigset_t set ; sigemptyset(&set ); int ret = sigismember(&set , SIGINT); if (ret == 0 ) { printf ("SIGINT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGINT 阻塞\n" ); } sigaddset(&set , SIGINT); sigaddset(&set , SIGQUIT); ret = sigismember(&set , SIGINT); if (ret == 0 ) { printf ("SIGINT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGINT 阻塞\n" ); } ret = sigismember(&set , SIGQUIT); if (ret == 0 ) { printf ("SIGQUIT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGQUIT 阻塞\n" ); } sigdelset(&set , SIGQUIT); ret = sigismember(&set , SIGQUIT); if (ret == 0 ) { printf ("SIGQUIT 不阻塞\n" ); } else if (ret == 1 ) { printf ("SIGQUIT 阻塞\n" ); } return 0 ; }
sigprocmask() 修改内核阻塞信号集 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int sigprocmask (int how, const sigset_t *set , sigset_t *oldset) ; - 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换) - 参数: - how : 如何对内核阻塞信号集进行处理 SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变; 假设内核中默认的阻塞信号集是mask, mask | set SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞; mask &= ~set SIG_SETMASK:覆盖内核中原来的值 - set :已经初始化好的用户自定义的信号集 - oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL - 返回值: 成功:0 失败:-1 设置错误号:EFAULT、EINVAL int sigpending(sigset_t *set ); - 功能:获取内核中的未决信号集 - 参数:set ,传出参数,保存的是内核中的未决信号集中的信息。
例子:
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 #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> int main () { sigset_t set ; sigemptyset(&set ); sigaddset(&set ,SIGINT); sigaddset(&set ,SIGQUIT); sigprocmask(SIG_BLOCK,&set ,NULL ); int num = 0 ; while (1 ){ num++; sigset_t pendingset; sigemptyset(&pendingset); sigpending(&pendingset); for (int i = 1 ; i < 32 ; i++) { if (sigismember(&pendingset,i) == 1 ){ printf ("1" ); }else if (sigismember(&pendingset,i) == 0 ){ printf ("0" ); }else { perror("sigprocmask" ); exit (0 ); } } printf ("\n" ); sleep(1 ); if (num == 10 ){ sigprocmask(SIG_UNBLOCK,&set ,NULL ); } } return 0 ; }
sigaction()信号捕捉函数
最好用sigaction,而不是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 #include <signal.h> int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact) ; - 功能:检查或者改变信号的处理。信号捕捉 - 参数: - signum : 需要捕捉的信号的编号或者宏值(信号的名称) - act :捕捉到信号之后的处理动作 - oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL - 返回值: 成功 0 失败 -1 struct sigaction { void (*sa_handler)(int ); void (*sa_sigaction)(int , siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void ); }; 1. 信号捕捉过程中会使用临时的阻塞信号集,处理完后,切换恢复到内核的阻塞信号集2. 在回调sa_handler函数的期间,默认会屏蔽该函数的调用,执行完之后,才能继续处理3. 信号集只是标志位,不能支持排队
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 #include <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> void myalarm (int num) { printf ("捕捉到了信号的编号是:%d\n" , num); printf ("xxxxxxx\n" ); } int main () { struct sigaction act ; act.sa_flags = 0 ; act.sa_handler = myalarm; sigemptyset(&act.sa_mask); sigaction(SIGALRM, &act, NULL ); struct itimerval new_value ; new_value.it_interval.tv_sec = 2 ; new_value.it_interval.tv_usec = 0 ; new_value.it_value.tv_sec = 3 ; new_value.it_value.tv_usec = 0 ; int ret = setitimer(ITIMER_REAL, &new_value, NULL ); printf ("定时器开始了...\n" ); if (ret == -1 ) { perror("setitimer" ); exit (0 ); } while (1 ); return 0 ; }
SIGCHLD信号 - 回收子进程
SIGCHLD 实现回收子进程 - 比较常用配合waitpid来批量回收子进程
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 #include <stdio.h> #include <sys/types.h> #include <signal.h> #include <wait.h> #include <unistd.h> #include <sys/stat.h> void myFun (int num) { printf ("捕捉到信号:%d\n" ,num); while (1 ){ int ret = waitpid(-1 ,NULL ,WNOHANG); if (ret > 0 ){ printf ("回收死亡的子进程:%d\n" ,ret); }else { break ; } } } int main () { sigset_t set ; sigemptyset(&set ); sigaddset(&set ,SIGCHLD); sigprocmask(SIG_BLOCK,&set ,NULL ); pid_t pid; for (int i = 0 ; i < 20 ; i++) { pid = fork(); if (pid == 0 ){ break ; } } if (pid >0 ){ struct sigaction act ; act.sa_flags = 0 ; act.sa_handler = myFun; sigaction(SIGCHLD,&act,NULL ); sigprocmask(SIG_UNBLOCK,&set ,NULL ); while (1 ){ printf ("parent process pid: %d\n" ,getpid()); sleep(2 ); } }else if (pid == 0 ){ printf ("child process pid: %d\n" ,getpid()); } return 0 ; }
2.8共享内存
共享内存允许两个或者多个进程共享物理内存的同一块区域 (通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入 (较少介入)。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快 。
共享内存使用步骤
调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符 。
使用 shmat() 来附上 共享内存段,即使该段成为调用进程的虚拟内存的一部分。
此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针 。
调用 shmdt() 来分离 共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
调用 shmctl() 来删除 共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。
共享内存函数 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 共享内存相关的函数 #include <sys/ipc.h> #include <sys/shm.h> int shmget (key_t key, size_t size, int shmflg) ; - 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。 新创建的内存段中的数据都会被初始化为0 - 参数: - key : key_t 类型是一个整形,通过这个找到或者创建一个共享内存。 一般使用16 进制表示,非0 值 - size: 共享内存的大小 - shmflg: 属性 - 访问权限 - 附加属性:创建/判断共享内存是不是存在 - 创建:IPC_CREAT - 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用 IPC_CREAT | IPC_EXCL | 0664 - 返回值: 失败:-1 并设置错误号 成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。 void *shmat (int shmid, const void *shmaddr, int shmflg) ; - 功能:和当前的进程进行关联 - 参数: - shmid : 共享内存的标识(ID),由shmget返回值获取 - shmaddr: 申请的共享内存的起始地址,指定NULL ,内核指定 - shmflg : 对共享内存的操作 - 读 : SHM_RDONLY, 必须要有读权限 - 读写: 0 - 返回值: 成功:返回共享内存的首(起始)地址。 失败(void *) -1 int shmdt (const void *shmaddr) ; - 功能:解除当前进程和共享内存的关联 - 参数: shmaddr:共享内存的首地址 - 返回值:成功 0 , 失败 -1 int shmctl (int shmid, int cmd, struct shmid_ds *buf) ; - 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。 - 参数: - shmid: 共享内存的ID - cmd : 要做的操作 - IPC_STAT : 获取共享内存的当前的状态 - IPC_SET : 设置共享内存的状态 - IPC_RMID: 标记共享内存被销毁 - buf:需要设置或者获取的共享内存的属性信息 - IPC_STAT : buf存储数据 - IPC_SET : buf中需要初始化数据,设置到内核中 - IPC_RMID : 没有用,NULL key_t ftok (const char *pathname, int proj_id) ; - 功能:根据指定的路径名,和int 值,生成一个共享内存的key - 参数: - pathname:指定一个存在的路径 /home/nowcoder/Linux/a.txt / - proj_id: int 类型的值,但是这系统调用只会使用其中的1 个字节8 个位 范围 : 0 -255 一般指定一个字符 'a'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 read_shm.c #include <stdio.h> #include <sys/shm.h> #include <sys/ipc.h> #include <memory.h> int main () { int shmid = shmget(100 ,4096 ,IPC_CREAT|0644 ); printf ("share memery id:%d\n" ,shmid); void *ptr = shmat(shmid,NULL ,0 ); printf ("%s\n" ,(char *)ptr); printf ("按任意键继续\n" ); getchar(); shmdt(ptr); shmctl(shmid,IPC_RMID,NULL ); 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 24 25 26 27 28 write_shm.c #include <stdio.h> #include <sys/shm.h> #include <sys/ipc.h> #include <memory.h> int main () { int shmid = shmget(100 ,4096 ,IPC_CREAT|0644 ); printf ("share memery id:%d\n" ,shmid); void *ptr = shmat(shmid,NULL ,0 ); char *str = "helloworld" ; memcpy (ptr,str,strlen (str)+1 ); printf ("按任意键继续\n" ); getchar(); shmdt(ptr); shmctl(shmid,IPC_RMID,NULL ); return 0 ; }
共享内存操作命令
ipcs 用法 ipcs -a // 打印当前系统中所有的进程间 通信方式的信息 ipcs -m // 打印出使用共享内存 进行进程间通信的信息 ipcs -q // 打印出使用消息队列 进行进程间通信的信息 ipcs -s // 打印出使用信号进行进程间通信 的信息
ipcrm 用法 ipcrm -M shmkey // 移除用shmkey创建的共享内存段 ipcrm -m shmid // 移除用shmid标识的共享内存段 ipcrm -Q msgkey // 移除用msqkey创建的消息队列 ipcrm -q msqid // 移除用msqid标识的消息队列 ipcrm -S semkey // 移除用semkey创建的信号 ipcrm -s semid // 移除用semid标识的信号
基础问题: 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 问题1:操作系统如何知道一块共享内存被多少个进程关联? - 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch - shm_nattach 记录了关联的进程个数 问题2:可不可以对共享内存进行多次删除 shmctl - 可以的 - 因为shmctl 标记删除共享内存,不是直接删除 - 什么时候真正删除呢? 当和共享内存关联的进程数为0的时候,就真正被删除 - 当共享内存的key为0的时候,表示共享内存被标记删除了 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能进行关联。 问题3:共享内存和内存映射的区别 1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外) 2.共享内存效果更高 3.内存 所有的进程操作的是同一块共享内存。 内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。 4.数据安全 - 进程突然退出 共享内存还存在 内存映射区消失 - 运行进程的电脑死机,宕机了 数据存在在共享内存中,没有了 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。 5.生命周期 - 内存映射区:进程退出,内存映射区销毁 - 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机 如果一个进程退出,会自动和共享内存进行取消关联。
2.9守护进程 终端 在 UNIX 系统中,用户通过终端登录系统后得到一个 shell 进程,这个终端成为 shell 进程的控制终端(Controlling Terminal) ,进程中,控制终端是保存在 PCB 中的信息,而 fork() 会复制 PCB 中的信息,因此由 shell 进程启动的其它进程的控制终端也是这个终端。
默认情况下(没有重定向),每个进程的标准输入、标准输出和标准错误输出都指向控制终端 ,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。
在控制终端输入一些特殊的控制键可以给前台进程发信号,例如 Ctrl + C 会产生 SIGINT 信号,Ctrl + \ 会产生 SIGQUIT 信号。
进程组 进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合 。进程组和会话是为支持 shell 作业控制而定义的抽象概念,用户通过 shell 能够交互式地在前台或后台运行命令。
进程组由一个或多个共享同一进程组标识符(PGID) 的进程组成。一个进程组拥有一个进程组首进程 ,该进程是创建该组的进程,其进程 ID 为该进程组的 ID,新进程会继承其父进程所属的进程组 ID。
进程组拥有一个生命周期,其开始时间为首进程创建组的时刻 ,结束时间为最后一个 成员进程退出组的时刻 。一个进程可能会因为终止而退出进程组,也可能会因为加入 了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
会话 ◼ 会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程 ID 会成为会 话 ID。新进程会继承其父进程的会话 ID(SID )。
◼ 一个会话中的所有进程共享单个控制终端 。控制终端会在会话首进程首次打开一个终 端设备时被建立。一个终端最多可能会成为一个 会话的控制终端。
◼ 在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为 后台进程组。只有前台进程组中的进程才能从控制终端中读取输入 。当用户在控制终 端中输入终端字符生成信号后,该信号会被发送到前台进程组中的所有成员。
◼ 当控制终端的连接建立起来之后,会话首进程会成为该终端的控制进程。
◼ pid_t getpgrp(void); ◼ pid_t getpgid(pid_t pid); ◼ int setpgid(pid_t pid, pid_t pgid); ◼ pid_t getsid(pid_t pid); ◼ pid_t setsid(void);
守护进程 ◼ 守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程 。它是一个生存期较长的进程,通常独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。一般采用以 d 结尾的名字。
◼ 守护进程具备下列特征:
生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭 。
它在后台运行并且不拥有控制终端 。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如 SIGINT、SIGQUIT)。
Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等
守护进程创建步骤 ◼ 执行一个 fork(),之后父进程退出,子进程继续执行。(1. 防止父进程死亡后出现shell提示符;2.防止该进程成为组进程的首进程)
◼ 子进程调用 setsid() 开启一个新会话。
◼ 清除进程的 umask 以确保当守护进程创建文件和目录时拥有所需的权限。 ◼ 修改进程的当前工作目录,通常会改为根目录(/)。 ◼ 关闭守护进程从其父进程继承而来的所有打开着的文件描述符。 ◼ 在关闭了文件描述符0、1、2之后,守护进程通常会打开/dev/null 并使用dup2() 使所有这些描述符指向这个设备。 ◼ 核心业务逻辑
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 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <fcntl.h> #include <sys/time.h> #include <signal.h> #include <string.h> #include <time.h> void myFun (int num) { time_t tm =time(NULL ); struct tm *loc = localtime(&tm); char * str = asctime(loc); int fd = open("a.txt" ,O_RDWR|O_CREAT|O_APPEND,0664 ); write(fd,str,strlen (str)); close(fd); } int main () { pid_t pid = fork(); if (pid > 0 ) exit (0 ); setsid(); umask(022 ); chdir("/home/klchen/" ); int fd = open("/dev/null" ,O_RDWR); dup2(fd,STDIN_FILENO); dup2(fd,STDOUT_FILENO); dup2(fd,STDERR_FILENO); struct sigaction act ; act.sa_flags = 0 ; act.sa_handler = myFun; sigemptyset(&act.sa_mask); sigaction(SIGALRM,&act,NULL ); struct itimerval val ; val.it_value.tv_sec = 2 ; val.it_value.tv_usec = 0 ; val.it_interval.tv_sec =2 ; val.it_interval.tv_usec =0 ; setitimer(ITIMER_REAL,&val,NULL ); while (1 ){ sleep(10 ); } return 0 ; }
3.Linux多线程开发 3.1线程概述 与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机 制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段。(传统意义上的 UNIX 进程只是多线程程序的一个特例,该进程只包含一个线程)
进程是 CPU 分配资源的最小单位,线程是操作系统调度执行的最小单位。
线程是轻量级的进程 (LWP :Light Weight Process),在 Linux 环境下线程的本 质仍是进程。
查看指定进程的 LWP 号:ps –Lf pid
共享资源 进程 ID 和父进程 ID 进程组 ID 和会话 ID 用户 ID 和 用户组 ID 文件描述符表 信号处置 文件系统的相关信息:文件权限掩码 (umask)、当前工作目录 虚拟地址空间(除栈、.text)//这两个会被划分
非共享资源 线程 ID 信号掩码 线程特有数据 error 变量 实时调度策略和优先级 栈,本地变量和函数的调用链接信息
查看当前 pthread 库版本:getconf GNU_LIBPTHREAD_VERSION
线程与进程的区别
进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
线程之间能够方便、快速地共享信息。只需将数据复制到共享(全局或堆)变量中即可。
创建线程比创建进程通常要快 10 倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。
3.2线程操作 线程创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 一般情况下,main函数所在的线程我们称之为主线程(main线程),其余创建的线程 称之为子线程。 程序中默认只有一个进程,fork()函数调用,2 个进程 程序中默认只有一个线程,pthread_create()函数调用,2 个线程。 #include <pthread.h> int pthread_create (pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg) ; - 功能:创建一个子线程 - 参数: - thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。 - attr : 设置线程的属性,一般使用默认值,NULL - start_routine : - arg : 给第三个参数使用,传参 - 返回值: 成功:0 失败:返回错误号。这个错误号和之前errno不太一样。 获取错误号的信息: char * strerror (int errnum) ;
例子-gcc pthread_create.c -o pthread_create -lpthread
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 <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void * callback (void * arg) { printf ("child thread...\n" ); printf ("arg: %d\n" ,*((int *)arg)); return NULL ; } int main () { pthread_t tid; int num = 10 ; int ret = pthread_create(&tid,NULL ,callback,(void *)&num); if (ret != 0 ){ char *error = strerror(ret); printf ("error: %s" ,error); } printf ("1\n" ); sleep(1 ); return 0 ; }
线程退出 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <pthread.h> void pthread_exit (void *retval) ; 功能:终止一个线程,在哪个线程中调用,就表示终止哪个线程 参数: retval:需要传递一个指针,作为一个返回值,可以在pthread_join()中获取到。(类似于return (void *)&value;) pthread_t pthread_self (void ) ; 功能:获取当前的线程的线程ID int pthread_equal (pthread_t t1, pthread_t t2) ; 功能:比较两个线程ID是否相等 不同的操作系统,pthread_t 类型的实现不一样,有的是无符号的长整型,有的 是使用结构体去实现的。
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 <stdio.h> #include <pthread.h> #include <string.h> void * callback (void * arg) { printf ("child thread id : %ld\n" , pthread_self()); return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char * errstr = strerror(ret); printf ("error : %s\n" , errstr); } for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } printf ("tid : %ld, main thread id : %ld\n" , tid ,pthread_self()); pthread_exit(NULL ); printf ("main thread exit\n" ); return 0 ; }
连接已终止的线程 - 线程回收 1 2 3 4 5 6 7 8 9 10 11 12 #include <pthread.h> int pthread_join (pthread_t thread, void **retval) ; - 功能:和一个已经终止的线程进行连接 回收子线程的资源 这个函数是阻塞函数,调用一次只能回收一个子线程 一般在主线程中使用 - 参数: - thread:需要回收的子线程的ID - retval: 接收子线程退出时的返回值 - 返回值: 0 : 成功 非0 : 失败,返回的错误号
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 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> int num = 10 ;void * callback (void *arg) { printf ("child thread: %ld\n" ,pthread_self()); pthread_exit((void *)&num); } int main () { pthread_t tid; int ret = pthread_create(&tid,NULL ,callback,NULL ); if (ret != 0 ){ char * error1 = strerror(ret); printf ("error: %s" ,error1); } for (int i =0 ;i != 5 ;i++){ printf ("i\n" ); } printf ("tid:%ld,main thread tid:%ld" ,tid,pthread_self()); int *thread_return; ret = pthread_join(tid,(void **)&thread_return); if (ret !=0 ) { char *error2 = strerror(ret); printf ("error: %s\n" ,error2); } printf ("exit data: %d\n" ,*thread_return); printf ("子线程回收成功!\n" ); pthread_exit(NULL ); return 0 ; }
线程分离 1 2 3 4 5 6 7 8 9 #include <pthread.h> int pthread_detach (pthread_t thread) ; - 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。 1. 不能多次分离,会产生不可预料的行为。 2. 不能去连接一个已经分离的线程,会报错。 - 参数:需要分离的线程的ID - 返回值: 成功:0 失败:返回错误号
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 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void * callback (void * arg) { printf ("chid thread id : %ld\n" , pthread_self()); return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char * errstr = strerror(ret); printf ("error1 : %s\n" , errstr); } printf ("tid : %ld, main thread id : %ld\n" , tid, pthread_self()); ret = pthread_detach(tid); if (ret != 0 ) { char * errstr = strerror(ret); printf ("error2 : %s\n" , errstr); } pthread_exit(NULL ); return 0 ; }
线程取消 1 2 3 4 5 6 #include <pthread.h> int pthread_cancel (pthread_t thread) ; - 功能:取消线程(让线程终止) 取消某个线程,可以终止某个线程的运行, 但是并不是立马终止,而是当子线程执行到一个取消点,线程才会终止。 取消点cancellaction point:系统规定好的一些系统调用,我们可以粗略的理解为从用户区到内核区的切换,这个位置称之为取消点。
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 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void * callback (void * arg) { printf ("chid thread id : %ld\n" , pthread_self()); for (int i = 0 ; i < 5 ; i++) { printf ("child : %d\n" , i); } return NULL ; } int main () { pthread_t tid; int ret = pthread_create(&tid, NULL , callback, NULL ); if (ret != 0 ) { char * errstr = strerror(ret); printf ("error1 : %s\n" , errstr); } pthread_cancel(tid); for (int i = 0 ; i < 5 ; i++) { printf ("%d\n" , i); } printf ("tid : %ld, main thread id : %ld\n" , tid, pthread_self()); pthread_exit(NULL ); return 0 ; }
线程属性 1 2 3 4 5 6 7 8 9 10 11 int pthread_attr_init (pthread_attr_t *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) ; - 设置线程分离的状态属性
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 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void * callback (void *arg) { printf ("child thread id: %ld\n" ,pthread_self()); return (NULL ); } int main () { pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); pthread_t tid; int ret = pthread_create(&tid,&attr,callback,NULL ); if (ret != 0 ){ char *errst = strerror(ret); printf ("error1:%s\n" ,errst); } printf ("tid:%ld,main thread id: %ld\n" ,tid,pthread_self()); pthread_attr_destroy(&attr); pthread_exit(NULL ); return 0 ; }
3.3线程同步
线程同步
线程的主要优势在于,能够通过全局变量来共享信息 。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
临界区 是指访问某一共享资源的代码片段 ,并且这段代码的执行应为原子操作 ,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。
线程同步 :即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
互斥量
为避免线程更新共享变量时出现问题,可以使用互斥量(mutex 是 mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共 享资源的原子访问。
互斥量有两种状态:已锁定(locked)和未锁(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法 。
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问
同一资源时将采用如下协议 : ⚫ 针对共享资源锁定互斥量 ⚫ 访问共享资源 ⚫ 对互斥量解锁
互斥锁 如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 互斥量的类型 pthread_mutex_t int pthread_mutex_init (pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) ; - 初始化互斥量 - 参数 : - mutex : 需要初始化的互斥量变量 - attr : 互斥量相关的属性,NULL - restrict : C语言的修饰符,被修饰的指针,不能由另外的一个指针进行操作。 pthread_mutex_t *restrict mutex = xxx; pthread_mutex_t * mutex1 = mutex; 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 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 #include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex;int ticket = 1000 ;void *sellticket (void *arg) { pthread_mutex_lock(&mutex); while (1 ){ if (ticket > 0 ){ printf ("%ld正在卖第 %d 张票\n" ,pthread_self(),ticket--); }else { pthread_mutex_unlock(&mutex); break ; } pthread_mutex_unlock(&mutex); usleep(6000 ); } } int main () { pthread_mutex_init(&mutex,NULL ); pthread_t tid1,tid2,tid3; pthread_create(&tid1,NULL ,sellticket,NULL ); pthread_create(&tid2,NULL ,sellticket,NULL ); pthread_create(&tid3,NULL ,sellticket,NULL ); pthread_join(tid1,NULL ); pthread_join(tid2,NULL ); pthread_join(tid3,NULL ); pthread_mutex_destroy(&mutex); pthread_exit(NULL ); 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 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 #include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex1, mutex2;void * workA (void * arg) { pthread_mutex_lock(&mutex1); sleep(1 ); pthread_mutex_lock(&mutex2); printf ("workA....\n" ); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL ; } void * workB (void * arg) { pthread_mutex_lock(&mutex2); sleep(1 ); pthread_mutex_lock(&mutex1); printf ("workB....\n" ); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex2); return NULL ; } int main () { pthread_mutex_init(&mutex1, NULL ); pthread_mutex_init(&mutex2, NULL ); pthread_t tid1, tid2; pthread_create(&tid1, NULL , workA, NULL ); pthread_create(&tid2, NULL , workB, NULL ); pthread_join(tid1, NULL ); pthread_join(tid2, NULL ); pthread_mutex_destroy(&mutex1); pthread_mutex_destroy(&mutex2); return 0 ; }
读写锁 读写锁 ◼ 当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的绝对排它性 ,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。 ◼ 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。 为了满足当前能够允许多个读出,但只允许一个写入的需求 ,线程提供了读写锁来实现。
读写锁的特点:
如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
如果有其它线程写数据,则其它线程都不允许读、写操作。
写是独占的,写的优先级高。
1 2 3 4 5 6 7 8 9 10 11 读写锁的类型 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) ;案例:8 个线程操作同一个全局变量。 3 个线程不定时写这个全局变量,5 个线程不定时的读这个全局变量
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 #include <stdio.h> #include <pthread.h> #include <unistd.h> int num =1 ;pthread_rwlock_t rwlock;void * writeNum (void *arg) { while (1 ) { pthread_rwlock_wrlock(&rwlock); num++; printf ("++write,tid: %ld,num: %d\n" ,pthread_self(),num); pthread_rwlock_unlock(&rwlock); usleep(100 ); } return NULL ; } void * readNum (void *arg) { while (1 ) { pthread_rwlock_rdlock(&rwlock); printf ("===read, tid : %ld, num : %d\n" , pthread_self(), num); pthread_rwlock_unlock(&rwlock); usleep(100 ); } return NULL ; } int main () { pthread_rwlock_init(&rwlock,NULL ); pthread_t wtid[3 ],rtid[5 ]; for (int i = 0 ; i < 3 ; i++) { pthread_create(&wtid[i],NULL ,writeNum,NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_create(&rtid[i],NULL ,readNum,NULL ); } for (int i = 0 ; i < 3 ; i++) { pthread_detach(wtid[i]); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(rtid[i]); } pthread_exit(NULL ); pthread_rwlock_destroy(&rwlock); 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 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 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> pthread_mutex_t mutex;struct Node { int num; struct Node *next ; }; struct Node *head = NULL ; void *producer (void *arg) { while (1 ){ pthread_mutex_lock(&mutex); struct Node *newNode = (struct Node *)malloc (sizeof (struct Node)); newNode ->next = head; head = newNode; newNode ->num = rand()%1000 ; printf ("add node, num: %d,tid: %ld\n" ,newNode->num,pthread_self()); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } void *custmer (void *arg) { while (1 ){ pthread_mutex_lock(&mutex); struct Node *tmp = head; if (head != NULL ){ printf ("del node,num: %d,tid: %ld\n" ,tmp->num,pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); usleep(100 ); }else { printf ("没了,快做\n" ); pthread_mutex_unlock(&mutex); usleep(100 ); } } return NULL ; } int main () { pthread_mutex_init(&mutex,NULL ); pthread_t ptid[5 ],ctid[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&ptid[i],NULL ,producer,NULL ); pthread_create(&ctid[i],NULL ,custmer,NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(ptid[i]); pthread_detach(ctid[i]); } pthread_exit(NULL ); pthread_mutex_destroy(&mutex); 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 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 #include <pthread.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <time.h> #include <sys/time.h> #include <sys/wait.h> #include <stdlib.h> #include <semaphore.h> int *buf;int bufSize = 100 ;int bufPtr;int count;sem_t full, empty, mutex; void *producer (void *arg) { while (bufPtr < bufSize) { sem_wait(&full); sem_wait(&mutex); buf[++bufPtr] = bufPtr; sem_post(&mutex); sem_post(&empty); } } void *consumer (void *arg) { while (1 ) { sem_wait(&empty); sem_wait(&mutex); count = (count + 1 ) % __INT32_MAX__; printf ("pid[%ld], count[%d], data[%d]\n" , pthread_self(), count, buf[bufPtr--]); sem_post(&mutex); sem_post(&full); } } int main () { sem_init(&full, 0 , bufSize); sem_init(&empty, 0 , 0 ); sem_init(&mutex, 0 , 1 ); bufPtr = -1 ; count = 0 ; buf = (int *)malloc (sizeof (int ) * bufSize); pthread_t ppid, cpids[5 ]; pthread_create(&ppid, NULL , producer, NULL ); for (int i = 0 ; i < 5 ; ++i) { pthread_create(&cpids[i], NULL , consumer, NULL ); } pthread_detach(ppid); for (int i = 0 ; i < 5 ; ++i) { pthread_detach(cpids[i]); } pthread_exit(NULL ); return 0 ; }
条件变量 某个条件满足之后开启或者解除阻塞;
《Liunx/UNIX系统编程手册》第531页有句话,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。发送信号时若无任何线程在等待该条件变量,这个也就会不了了之。线程如在此后等待该条件变量,只有当再次收到此变量的下一信号时,方可解除阻塞状态。
1 2 3 4 5 6 7 8 9 10 11 条件变量的类型 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) ; - 唤醒
例子:资源不足的时候,条件变量wait等待生产者,生产者生产后,条件变量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 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 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> pthread_mutex_t mutex; pthread_cond_t cond;struct Node { int num; struct Node *next ; }; struct Node *head = NULL ; void *producer (void *arg) { while (1 ){ pthread_mutex_lock(&mutex); struct Node *newNode = (struct Node *)malloc (sizeof (struct Node)); newNode ->next = head; head = newNode; newNode ->num = rand()%1000 ; printf ("add node, num: %d,tid: %ld\n" ,newNode->num,pthread_self()); pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); usleep(100 ); } return NULL ; } void *custmer (void *arg) { while (1 ){ pthread_mutex_lock(&mutex); struct Node *tmp = head; if (head != NULL ){ head = head ->next; printf ("del node,num: %d,tid: %ld\n" ,tmp->num,pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); usleep(100 ); }else { pthread_cond_wait(&cond,&mutex); printf ("没了,快做\n" ); pthread_mutex_unlock(&mutex); } } return NULL ; } int main () { pthread_mutex_init(&mutex,NULL ); pthread_cond_init(&cond,NULL ); pthread_t ptid[5 ],ctid[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&ptid[i],NULL ,producer,NULL ); pthread_create(&ctid[i],NULL ,custmer,NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(ptid[i]); pthread_detach(ctid[i]); } pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); pthread_exit(NULL ); 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 信号量的类型 sem_t #include <semaphore.h> int sem_init (sem_t *sem, int pshared, unsigned int value) ; - 初始化信号量 - 参数: - sem : 信号量变量的地址 - pshared : 0 用在线程间 ,非0 用在进程间 - value : 信号量中的值 int sem_destroy (sem_t *sem) ; - 释放资源 int sem_wait (sem_t *sem) ; - 对信号量加锁,调用一次对信号量的值-1 ,如果值为0 ,就阻塞 int sem_trywait (sem_t *sem) ;int sem_timedwait (sem_t *sem, const struct timespec *abs_timeout) ;int sem_post (sem_t *sem) ; - 对信号量解锁,调用一次对信号量的值+1 int sem_getvalue (sem_t *sem, int *sval) ;sem_t psem;sem_t csem;init(psem, 0 , 8 ); init(csem, 0 , 0 ); producer() { sem_wait(&psem); sem_post(&csem) } customer() { sem_wait(&csem); sem_post(&psem) }
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 #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h> #include <semaphore.h> pthread_mutex_t mutex;sem_t psem;sem_t csem;struct Node { int num; struct Node *next ; }; struct Node * head = NULL ;void * producer (void * arg) { while (1 ) { sem_wait(&psem); pthread_mutex_lock(&mutex); struct Node * newNode = (struct Node *)malloc (sizeof (struct Node)); newNode->next = head; head = newNode; newNode->num = rand() % 1000 ; printf ("add node, num : %d, tid : %ld\n" , newNode->num, pthread_self()); pthread_mutex_unlock(&mutex); sem_post(&csem); } return NULL ; } void * customer (void * arg) { while (1 ) { sem_wait(&csem); pthread_mutex_lock(&mutex); struct Node * tmp = head; head = head->next; printf ("del node, num : %d, tid : %ld\n" , tmp->num, pthread_self()); free (tmp); pthread_mutex_unlock(&mutex); sem_post(&psem); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); sem_init(&psem, 0 , 8 ); sem_init(&csem, 0 , 0 ); pthread_t ptids[5 ], ctids[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&ptids[i], NULL , producer, NULL ); pthread_create(&ctids[i], NULL , customer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_detach(ptids[i]); pthread_detach(ctids[i]); } while (1 ) { sleep(10 ); } pthread_mutex_destroy(&mutex); pthread_exit(NULL ); return 0 ; }
4.Linux网络编程 4.1网络基础 看pdf
4.2 socket通信基础 socket简介 所谓 socket(套接字),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象 。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。
socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制 。
socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件 。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地 进程间通信,而套接字多应用于网络进程间 数据的传递。
1 2 3 4 5 - 服务器端:被动接受连接,一般不会主动发起连接 - 客户端:主动向服务器发起连接 socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别。
字节序 现代 CPU 的累加器 一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数。那么这 4字节在内存中排列的顺序 将影响它被累加器装载成的整数的值,这就是字节序问题。在各种计算机体系结构中,对于字节、字等的存储机制 有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如 果不达成一致的规则,通信双方将无法进行正确的编码/译码从而导致通信失败。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数 据当然就无需谈顺序的问题了)。
字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整 数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地 址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地 址处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> int main () { union { short value; char bytes[sizeof (short )]; }test; test.value = 0x0102 ; if ((test.bytes[0 ] == 1 )&&(test.bytes[1 ] == 2 )){ printf ("大端字节序\n" ); }else if ((test.bytes[0 ] == 2 )&&(test.bytes[1 ] == 1 )){ printf ("小端字节序\n" ); }else { printf ("未知\n" ); } return 0 ; }
字节序转换函数
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之。 解决问题的方法是: 发送端总是把要发送的数据转换成大端字节序数据后再发送 ,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。
网络字节顺序 是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关 ,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。 BSD Socket提供了封装好的转换接口,方便程序员使用。
从主机字节序到网络字节序的转换函数:htons、htonl;
从网络字节序到主机字节序的转换函数:ntohs、ntohl。
1 2 3 4 5 6 7 8 9 10 11 12 13 h - host 主机,主机字节序 to - 转换成什么 n - network 网络字节序 s - short unsigned short l - long unsigned int #include <arpa/inet.h> uint16_t htons (uint16_t hostshort) ; uint16_t ntohs (uint16_t netshort) ; uint32_t htonl (uint32_t hostlong) ; uint32_t ntohl (uint32_t netlong) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> #include <arpa/inet.h> int main () { unsigned short a = 0x0102 ; printf ("%x\n" ,a); unsigned short b = htons(a); printf ("%x\n" ,b); char buf[4 ] = {196 ,168 ,1.100 }; int num = *(int *)buf; int sum = htonl(num); unsigned char *p = (char *)∑ printf ("%d %d %d %d\n" ,*p,*(p+1 ),*(p+2 ),*(p+3 )); return 0 ; }
socket地址 客户端和服务器通信,需要IP Port ….,所以封装好了一个socket地址。
// socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
通用socket地址 socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
1 2 3 4 5 6 #include <bits/socket.h> struct sockaddr {sa_family_t sa_family;char sa_data[14 ];}; typedef unsigned short int sa_family_t ;
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议 族(protocol family,也称 domain)和对应的地址族入下所示
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
1 2 3 4 5 6 7 8 #include <bits/socket.h> struct sockaddr_storage { sa_family_t sa_family;unsigned long int __ss_align;char __ss_padding[ 128 - sizeof (__ss_align) ];}; typedef unsigned short int sa_family_t ;
专用socket地址 很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr *退化成了(void *)**的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再 *强制类型转化为所需的地址类型**。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
1 2 3 4 5 6 #include <sys/un.h> struct sockaddr_un { sa_family_t sin_family;char sun_path[108 ];}
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和IPv6
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 <netinet/in.h> struct sockaddr_in //这个用较多{ sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t ) - sizeof (struct in_addr)]; }; socklen_t 是sockaddr_in的长度类型struct in_addr { in_addr_t s_addr; }; struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr ; uint32_t sin6_scope_id; }; typedef unsigned short uint16_t ;typedef unsigned int uint32_t ;typedef uint16_t in_port_t ;typedef uint32_t in_addr_t ;#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
4.3 IP地址转换 字符串ip-整数 ,主机-网络字节序的转换
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串 表示 IPv4 地址,以及用十六进制字符串 表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用 。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:
旧接口- 以下函数比较久,且只使用于IPv4,而且是不可重用的函数,比如第三个因为直接用了那个结构体变量
1 2 3 4 5 6 7 8 #include <arpa/inet.h> in_addr_t inet_addr (const char *cp) ;int inet_aton (const char *cp, struct in_addr *inp) ;char *inet_ntoa (struct in_addr in) ;
新接口-下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ;af:地址族: AF_INET AF_INET6 src:需要转换的点分十进制的IP字符串 dst:转换后的结果保存在这个里面 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 #include <stdio.h> #include <arpa/inet.h> int main () { char buf[] = "192.163.1.4" ; unsigned int num = 0 ; inet_pton(AF_INET,buf,&num); unsigned char *p = (unsigned char *)# printf ("%d %d %d %d\n" ,*p,*(p+1 ),*(p+2 ),*(p+3 )); char ip[16 ] = "" ; const char *str = inet_ntop(AF_INET,&num,ip,16 ); printf ("str:%s\n" ,str); printf ("ip:%s\n" ,ip); printf ("%d\n" ,ip == str); return 0 ; }
4.4TCP通信流程 1 2 3 4 5 6 7 8 9 10 11 UDP:用户数据报协议,面向无连接,可以单播,多播,广播, 面向数据报,不可靠 TCP:传输控制协议,面向连接的,可靠的,基于字节流,仅支持单播传输 UDP TCP 是否创建连接 无连接 ||面向连接 是否可靠 不可靠 ||可靠的 连接的对象个数 一对一、一对多、多对一、多对多 ||支持一对一 传输的方式 面向数据报 ||面向字节流 首部开销 8 个字节 ||最少20 个字节 适用场景 实时应用(视频会议,直播) ||可靠性高的应用(文件传输)
TCP通信流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1. 创建一个用于监听的套接字 - 监听:监听有客户端的连接 - 套接字:这个套接字其实就是一个文件描述符 2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息) - 客户端连接服务器的时候使用的就是这个IP和端口 3. 设置监听,监听的fd开始工作4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd) 5. 通信 - 接收数据 - 发送数据 6. 通信结束,断开连接
1 2 3 4 5 6 7 1. 创建一个用于通信的套接字(fd)2. 连接服务器,需要指定连接的服务器的 IP 和 端口3. 连接成功了,客户端可以直接和服务器通信 - 接收数据 - 发送数据 4. 通信结束,断开连接
4.4套接字函数 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 #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 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败:-1 int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ; - 功能:绑定,将fd 和本地的IP + 端口进行绑定 - 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小 int listen (int sockfd, int backlog) ; - 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值, 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) ;
4.5 TCP通信并发实现 TCP通信 单连
服务器端设置端口999;客户端的端口是随机分配的,只要去连接服务器端的999就行
服务器端要去连的IP是任意的;客户端要去连接的IP是服务器端的192.168.31.128
服务器端 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int lfd = socket(AF_INET,SOCK_STREAM,0 ); if (lfd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof (saddr)); if (ret == -1 ){ perror("bind" ); exit (-1 ); } ret = listen(lfd,8 ); if (ret == -1 ){ perror("listen" ); exit (-1 ); } struct sockaddr_in clientaddr ; int len = sizeof (clientaddr); int cfd = accept(lfd,(struct sockaddr *)&clientaddr,&len); if (cfd == -1 ){ perror("accept" ); exit (-1 ); } char clientIP[16 ]; inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clientIP,sizeof (clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf ("client ip is %s, port is %d\n" , clientIP, clientPort); char recvBuf[1024 ] = {0 }; while (1 ){ int num = read(cfd,recvBuf,sizeof (recvBuf)); if (ret == -1 ){ perror("read" ); exit (-1 ); }else if (num > 0 ){ printf ("recv client data: %s\n" ,recvBuf); }else if (num == 0 ){ printf ("client closed..." ); break ; } char * data = "连上了,我是服务器端" ; write(cfd,data,strlen (data)); } close(cfd); close(lfd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main () { int fd = socket(AF_INET,SOCK_STREAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in serveraddr ; serveraddr.sin_family = AF_INET; inet_pton(AF_INET,"192.168.31.128" ,&serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9999 ); int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof (serveraddr)); if (ret == -1 ){ perror("connect" ); exit (-1 ); } char recvBuf[1024 ]={0 }; while (1 ){ char *data = "这是客户端" ; write(fd,data,strlen (data)); sleep(1 ); int len = read(fd,recvBuf,sizeof (recvBuf)+1 ); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len > 0 ){ printf ("recv server data:%s\n" ,recvBuf); }else if (len == 0 ){ printf ("server closed...\n" ); break ; } } close(fd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int lfd = socket(AF_INET,SOCK_STREAM,0 ); if (lfd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9988 ); int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof (saddr)); if (ret == -1 ){ perror("bind" ); exit (-1 ); } ret = listen(lfd,8 ); if (ret == -1 ){ perror("listen" ); exit (-1 ); } struct sockaddr_in clientaddr ; int len = sizeof (clientaddr); int cfd = accept(lfd,(struct sockaddr *)&clientaddr,&len); if (cfd == -1 ){ perror("accept" ); exit (-1 ); } char clientIP[16 ]; inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clientIP,sizeof (clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf ("client ip is %s, port is %d\n" , clientIP, clientPort); char recvBuf[1024 ] = {0 }; while (1 ){ int num = read(cfd,recvBuf,sizeof (recvBuf)); if (ret == -1 ){ perror("read" ); exit (-1 ); }else if (num > 0 ){ printf ("recv client data: %s\n" ,recvBuf); }else if (num == 0 ){ printf ("client closed..." ); break ; } write(cfd,recvBuf,strlen (recvBuf)); } close(cfd); close(lfd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main () { int fd = socket(AF_INET,SOCK_STREAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in serveraddr ; serveraddr.sin_family = AF_INET; inet_pton(AF_INET,"192.168.31.128" ,&serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9988 ); int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof (serveraddr)); if (ret == -1 ){ perror("connect" ); exit (-1 ); } char recvBuf[1024 ]={0 }; while (1 ){ char data[1024 ]; memset (data,0 ,sizeof (data)); printf ("请输入\n" ); scanf ("%s" ,data); write(fd,data,strlen (data)); sleep(1 ); int len = read(fd,recvBuf,sizeof (recvBuf)+1 ); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len > 0 ){ printf ("recv server data:%s\n" ,recvBuf); }else if (len == 0 ){ printf ("server closed...\n" ); break ; } } close(fd); return 0 ; }
三次握手 TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等 。
TCP 可以看成是一种字节流,它会处理 IP 层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部。
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用四次挥手 来关闭一个连接。 三次握手的目的是保证双方互相之间建立了连接。 三次握手发生在客户端连接的时候,当调用connect(),底层会通过TCP协议进行三次握手
TCP报文头
16 位端口号(port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协议或 应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统自动选择的临时端口号
32 位序号(sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输 方向上的字节流的每个字节的编号。假设主机 A 和主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值被系统初始化为某个随机值 ISN (Initial Sequence Number,初始序号 值)。那么在该传输方向上(从 A 到 B),后续的 TCP 报文段中序号值将被系统设置成 ISN 加上 该报文段所携带数据的第一个字节在整个字节流中的偏移 。例如,某个 TCP 报文段传送的数据是字 节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从 B 到 A)的 TCP 报文段的序号值也具有相同的含义
32 位确认号(acknowledgement number):用作对另一方发送来的 TCP 报文段的响应。其值是 收到的 TCP 报文段的序号值 + 标志位长度(SYN,FIN) + 数据长度 。假设主机 A 和主机 B 进行 TCP 通信,那么 A 发送出的 TCP 报文段不仅携带自己的序号,而且包含对 B 发送来的 TCP 报文段 的确认号。反之,B 发送出的 TCP 报文段也同样携带自己的序号和对 A 发送来的报文段的确认序号。
6 位标志位包含如下几项:
URG 标志,表示紧急指针(urgent pointer)是否有效。
ACK 标志,表示确认号是否有效。我们称携带 ACK 标志的 TCP 报文段为确认报文段。
PSH 标志,提示接收端应用程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾 出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
RST 标志,表示要求对方重新建立连接。我们称携带 RST 标志的 TCP 报文段为复位报文段。
SYN 标志,表示请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为同步报文段。
FIN 标志,表示通知对方本端要关闭连接了。我们称携带 FIN 标志的 TCP 报文段为结束报文 段。
16 位窗口大小(window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是接收 通告窗口 (Receiver Window,RWND)。它告诉对方本端的 TCP 接收缓冲区还能容纳多少 字节的数据 ,这样对方就可以控制发送数据的速度
16 位校验和(TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法以校验 TCP 报文段在传输过程中是否损坏。注意,这个校验不仅包括 TCP 头部,也包括数据部分。 这也是 TCP 可靠传输的一个重要保障
16 位紧急指针(urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一 个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏 移,不妨称之为紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
三次握手连接过程 1 2 3 4 5 6 7 8 9 10 11 第一次握手: 1. 客户端将SYN标志位置为1 2. 生成一个随机的32 位的序号seq=J , 这个序号后边第三次握手的时候是可以携带数据(数据的大小) 第二次握手: 1. 服务器端接收客户端的连接: ACK=1 2. 服务器会回发一个确认序号: ack=客户端的序号 + 数据长度 + SYN/FIN(按一个字节算) 3. 服务器端会向客户端发起连接请求: SYN=1 4. 服务器会生成一个随机序号:seq = K 第三次握手: 1. 客户单应答服务器的连接请求:ACK=1 2. 客户端回复收到了服务器端的数据:ack=服务端的序号 + 数据长度 + SYN/FIN(按一个字节算)
TCP滑动窗口 滑动窗口(Sliding window)是一种流量控制技术。早期的网络通信中,通信双方不会考虑网络的 拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包 , 谁也发不了数据,所以就有了滑动窗口机制来解决此问题。滑动窗口协议是用来改善吞吐量的一种 技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包 (称窗口尺寸)。 TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于 接收数据 。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报。
滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构
窗口理解为缓冲区的大小 滑动窗口的大小会随着发送数据和接收数据而变化。 通信的双方都有发送缓冲区和接收数据的缓冲区 服务器: 发送缓冲区(发送缓冲区的窗口) 接收缓冲区(接收缓冲区的窗口) 客户端 发送缓冲区(发送缓冲区的窗口) 接收缓冲区(接收缓冲区的窗口)
1 2 3 4 5 6 7 发送方的缓冲区: 白色格子:空闲的空间 灰色格子:数据已经被发送出去了,但是还没有被接收 紫色格子:还没有发送出去的数据 接收方的缓冲区: 白色格子:空闲的空间 紫色格子:已经接收到的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # mss: Maximum Segment Size(一条数据的最大的数据量) # win: 滑动窗口 1. 客户端向服务器发起连接,客户单的滑动窗口是4096 ,一次发送的最大数据量是1460 2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144 ,一次发送的最大数据量是1024 3. 第三次握手4. 4 -9 客户端连续给服务器发送了6 k的数据,每次发送1 k5. 第10 次,服务器告诉客户端:发送的6 k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2 k,窗口大小是2 k 6. 第11 次,服务器告诉客户端:发送的6 k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4 k,窗口大小是4 k 7. 第12 次,客户端给服务器发送了1 k的数据8. 第13 次,客户端主动请求和服务器断开连接,并且给服务器发送了1 k的数据9. 第14 次,服务器回复ACK 8194 , a:同意断开连接的请求 b:告诉客户端已经接受到方才发的2 k的数据c:滑动窗口2 k 10. 第15 、16 次,通知客户端滑动窗口的大小11. 第17 次,第三次挥手,服务器端给客户端发送FIN,请求断开连接12. 第18 次,第四次挥手,客户端同意了服务器端的断开请求
四次挥手 四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。 客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。 因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。
多进程实现并发服务器 1 2 3 4 5 6 要实现TCP通信服务器处理并发的任务,使用多线程或者多进程来解决。 思路: 1. 一个父进程,多个子进程 2. 父进程负责等待并接受客户端的连接 3. 子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信
server_process 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <signal.h> #include <wait.h> void recycleChild (int arg) { while (1 ){ int ret = waitpid(-1 ,NULL ,WNOHANG); if (ret == -1 ){ break ; }else if (ret == 0 ){ break ; }else if (ret >0 ){ printf ("子进程%d被回收了\n" ,ret); } } } int main () { struct sigaction act ; act.sa_flags = 0 ; sigemptyset(&act.sa_mask); act.sa_handler = recycleChild; sigaction(SIGCHLD,&act,NULL ); int lfd = socket(AF_INET,SOCK_STREAM,0 ); if (lfd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9898 ); int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof (saddr)); if (ret == -1 ){ perror("bind" ); exit (-1 ); } ret = listen(lfd,128 ); if (ret == -1 ){ perror("listen" ); exit (-1 ); } while (1 ){ struct sockaddr_in clientaddr ; int len = sizeof (clientaddr); int cfd = accept(lfd,(struct sockaddr *)&clientaddr,&len); if (cfd == -1 ){ perror("accept" ); exit (-1 ); } pid_t pid = fork(); if (pid == 0 ){ char clientIP[16 ]; inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clientIP,sizeof (clientIP)); unsigned short clientPort = ntohs(clientaddr.sin_port); printf ("client ip is %s, port is %d\n" , clientIP, clientPort); char recvBuf[1024 ]; while (1 ){ int num = read(cfd,recvBuf,sizeof (recvBuf)); if (ret == -1 ){ perror("read" ); exit (-1 ); }else if (num > 0 ){ printf ("recv client data: %s\n" ,recvBuf); }else if (num == 0 ){ printf ("client closed...\n" ); break ; } write(cfd,recvBuf,strlen (recvBuf)+1 ); } close(cfd); exit (0 ); } } close(lfd); return 0 ; }
client 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main () { int fd = socket(AF_INET,SOCK_STREAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in serveraddr ; serveraddr.sin_family = AF_INET; inet_pton(AF_INET,"192.168.31.128" ,&serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9898 ); int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof (serveraddr)); if (ret == -1 ){ perror("connect" ); exit (-1 ); } char recvBuf[1024 ]; int i = 0 ; while (1 ){ sprintf (recvBuf,"recvBuf: %d\n" ,i++); write(fd,recvBuf,strlen (recvBuf)+1 ); int len = read(fd,recvBuf,sizeof (recvBuf)+1 ); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len > 0 ){ printf ("recv server data:%s\n" ,recvBuf); }else if (len == 0 ){ printf ("server closed...\n" ); break ; } sleep(1 ); } close(fd); return 0 ; }
多线程实现并发服务器 - 类似于线程池 server_process 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> struct sockInfo { int fd; struct sockaddr_in addr ; pthread_t tid; }; struct sockInfo sockinfos [128];void * working (void * arg) { struct sockInfo *pinfo = (struct sockInfo *)arg; char recvBuf[1024 ]; while (1 ){ int num = read(pinfo->fd,recvBuf,sizeof (recvBuf)); if (num == -1 ){ perror("read" ); exit (-1 ); }else if (num > 0 ){ printf ("recv client data: %s\n" ,recvBuf); }else if (num == 0 ){ printf ("client closed...\n" ); break ; } write(pinfo->fd,recvBuf,strlen (recvBuf)+1 ); } close(pinfo->fd); return NULL ; } int main () { int lfd = socket(AF_INET,SOCK_STREAM,0 ); if (lfd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9898 ); int ret = bind(lfd,(struct sockaddr *)&saddr,sizeof (saddr)); if (ret == -1 ){ perror("bind" ); exit (-1 ); } ret = listen(lfd,128 ); if (ret == -1 ){ perror("listen" ); exit (-1 ); } int max = sizeof (sockinfos)/sizeof (sockinfos[0 ]); for (int i=0 ;i<max;i++){ bzero(&sockinfos[i],sizeof (sockinfos[i])); sockinfos[i].fd=-1 ; sockinfos[i].tid=-1 ; } while (1 ){ struct sockaddr_in cliaddr ; int len = sizeof (cliaddr); int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len); struct sockInfo *pinfo ; for (int i=0 ;i<max;i++){ if (sockinfos[i].tid == -1 ){ pinfo = &sockinfos[i]; break ; } if (i == max -1 ){ sleep(1 ); i=-1 ; } } pinfo->fd = cfd; memcpy (&pinfo->addr,&cliaddr,len); pthread_create(&pinfo->tid,NULL ,working,pinfo); pthread_detach(pinfo->tid); } close(lfd); return 0 ; }
client - 和前面一样 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <string.h> #include <stdlib.h> int main () { int fd = socket(AF_INET,SOCK_STREAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in serveraddr ; serveraddr.sin_family = AF_INET; inet_pton(AF_INET,"192.168.31.128" ,&serveraddr.sin_addr.s_addr); serveraddr.sin_port = htons(9898 ); int ret = connect(fd,(struct sockaddr*)&serveraddr,sizeof (serveraddr)); if (ret == -1 ){ perror("connect" ); exit (-1 ); } char recvBuf[1024 ]; int i = 0 ; while (1 ){ sprintf (recvBuf,"recvBuf: %d\n" ,i++); write(fd,recvBuf,strlen (recvBuf)+1 ); int len = read(fd,recvBuf,sizeof (recvBuf)+1 ); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len > 0 ){ printf ("recv server data:%s\n" ,recvBuf); }else if (len == 0 ){ printf ("server closed...\n" ); break ; } sleep(1 ); } close(fd); return 0 ; }
TCP状态转换
2MSL(Maximum Segment Lifetime) 主动断开连接的一方, 最后进入一个 TIME_WAIT状态, 这个状态会持续: 2msl
1 2 3 4 5 6 当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方 必须处于TIME_WAIT 状态并持续 2 MSL 时间。 这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。 主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号, 被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。
1 2 3 当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发 送的数据,但是 A 已经不能再向 B 发送数据。
从程序的角度,可以使用 API 来控制实现半连接状态 :
1 2 3 4 5 6 7 8 9 10 11 #include <sys/socket.h> int shutdown (int sockfd, int how) ;sockfd: 需要关闭的socket的描述符 how: 允许为shutdown操作选择以下几种方式: SHUT_RD(0 ): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。 该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 SHUT_WR(1 ): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发 出写操作。 SHUT_RDWR(2 ):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。
使用 close 中止一个连接,但它只是减少描述符的引用计数 ,并不直接关闭连接,只有当描述符的引用 计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符 。也可选择中止一个方 向的连接,只中止读或只中止写。 注意:
如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用 进程都调用了 close,套接字将被释放。
在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。 但如果一个进程 close(sfd) 将不会影响到其它进程。
端口复用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #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 - SO_REUSEPORT - optval : 端口复用的值(整形) - 1 : 可以复用 - 0 : 不可以复用 - optlen : optval参数的大小 端口复用,设置的时机是在服务器绑定bind端口之前。 setsockopt(); bind();
查看网络相关信息的命令 netstat 参数:-a 所有的socket -p 显示正在使用socket的程序的名称 -n 直接使用IP地址,而不通过域名服务器
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 #include <stdio.h> #include <ctype.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> int main (int argc, char *argv[]) { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("socket" ); return -1 ; } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); int optval = 1 ; setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof (optval)); int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); if (ret == -1 ) { perror("bind" ); return -1 ; } ret = listen(lfd, 8 ); if (ret == -1 ) { perror("listen" ); return -1 ; } struct sockaddr_in cliaddr ; socklen_t len = sizeof (cliaddr); int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len); if (cfd == -1 ) { perror("accpet" ); return -1 ; } char cliIp[16 ]; inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof (cliIp)); unsigned short cliPort = ntohs(cliaddr.sin_port); printf ("client's ip is %s, and port is %d\n" , cliIp, cliPort ); char recvBuf[1024 ] = {0 }; while (1 ) { int len = recv(cfd, recvBuf, sizeof (recvBuf), 0 ); if (len == -1 ) { perror("recv" ); return -1 ; } else if (len == 0 ) { printf ("客户端已经断开连接...\n" ); break ; } else if (len > 0 ) { printf ("read buf = %s\n" , recvBuf); } for (int i = 0 ; i < len; ++i) { recvBuf[i] = toupper (recvBuf[i]); } printf ("after buf = %s\n" , recvBuf); ret = send(cfd, recvBuf, strlen (recvBuf) + 1 , 0 ); if (ret == -1 ) { perror("send" ); return -1 ; } } close(cfd); close(lfd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int fd = socket(PF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror("socket" ); return -1 ; } struct sockaddr_in seraddr ; inet_pton(AF_INET, "127.0.0.1" , &seraddr.sin_addr.s_addr); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(9999 ); int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret == -1 ){ perror("connect" ); return -1 ; } while (1 ) { char sendBuf[1024 ] = {0 }; fgets(sendBuf, sizeof (sendBuf), stdin ); write(fd, sendBuf, strlen (sendBuf) + 1 ); int len = read(fd, sendBuf, sizeof (sendBuf)); if (len == -1 ) { perror("read" ); return -1 ; }else if (len > 0 ) { printf ("read buf = %s\n" , sendBuf); } else { printf ("服务器已经断开连接...\n" ); break ; } } close(fd); return 0 ; }
4.6 IO多路复用
I/O 多路复用使得程序能同时监听多个文件描述符 ,能够提高程序的性能,Linux 下实现 I/O 多路复用的 系统调用主要有 select、poll 和 epoll
IO多路复用概述 又叫IO多路转接,IO指的是针对内存
select
主旨思想:
首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O 操作时,该函数才返回。 a.这个函数是阻塞 b.函数对文件描述符的检测的操作是由内核完成的
在返回时,它会告诉进程有多少(哪些)描述符要进行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 26 27 28 29 30 31 32 33 34 35 36 37 #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/select.h> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ;- 参数: - nfds : 委托内核检测的最大文件描述符的值 + 1 - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性 - 一般检测读操作 - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区 - 是一个传入传出参数 - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性 - - exceptfds : 检测发生异常的文件描述符的集合 - ti meout : 设置的超时时间 struct timeval { long tv_sec; long tv_usec; }; - NULL : 永久阻塞,直到检测到了文件描述符有变化 - tv_sec = 0 tv_usec = 0 , 不阻塞 - tv_sec > 0 tv_usec > 0 , 阻塞对应的时间 - 返回值 : - >1 : 失败 - >0 (n) : 检测的集合中有n个文件描述符发生了变化 void FD_CLR(int fd, fd_set *set );int FD_ISSET (int fd, fd_set *set ) ;void FD_SET (int fd, fd_set *set ) ;void FD_ZERO (fd_set *set ) ;
对应 3 4 100 101的fd:
先定义reads 1024bit(0-1023),然后监听 3 4 100 101的fd放到reads中
用select把reads读到内核中,并且检测,检测到有数据的置位1,没数据置位0,(比如只有3 4 有数据)
然后从内核态拷贝到用户态(用户态遍历之后知道3 4 有数据),然后对有数据的进行通信
select实现多路转接 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 # include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/select.h> #include <arpa/inet.h> #include <stdlib.h> int main () { int lfd = socket(AF_INET,SOCK_STREAM,0 ); struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999 ); saddr.sin_addr.s_addr = INADDR_ANY; bind(lfd,(struct sockaddr *)&saddr,sizeof (saddr)); listen(lfd,8 ); fd_set rdset,tmp; FD_ZERO(&rdset); FD_SET(lfd,&rdset); int maxfd = lfd; while (1 ){ tmp = rdset; int ret = select(maxfd+1 ,&tmp,NULL ,NULL ,NULL ); if (ret == -1 ){ perror("select" ); exit (-1 ); }else if (ret == 0 ){ continue ; }else if (ret > 0 ){ if (FD_ISSET(lfd,&tmp)){ struct sockaddr_in cliaddr; int len = sizeof (cliaddr); int cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len); FD_SET(cfd,&rdset); maxfd = maxfd > cfd ? maxfd : cfd; } for (int i = lfd+1 ;i <= maxfd;i++){ if (FD_ISSET(i,&tmp)){ char buf[1024 ] = {0 }; int len = read(i,&buf,sizeof (buf)); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); FD_CLR(i,&rdset); }else if (len > 0 ){ printf ("read buf = %s\n" ,buf); write(i,buf,strlen (buf)+1 ); } } } } } close(lfd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int fd = socket(PF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror("socket" ); return -1 ; } struct sockaddr_in seraddr ; inet_pton(AF_INET, "127.0.0.1" , &seraddr.sin_addr.s_addr); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(9999 ); int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret == -1 ){ perror("connect" ); return -1 ; } int num = 0 ; while (1 ) { char sendBuf[1024 ] = {0 }; sprintf (sendBuf, "send data %d" , num++); write(fd, sendBuf, strlen (sendBuf) + 1 ); int len = read(fd, sendBuf, sizeof (sendBuf)); if (len == -1 ) { perror("read" ); return -1 ; }else if (len > 0 ) { printf ("read buf = %s\n" , sendBuf); } else { printf ("服务器已经断开连接...\n" ); break ; } sleep(1 ); } close(fd); return 0 ; }
poll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <poll.h> struct pollfd {int fd; short events; short revents; }; struct pollfd myfd ;myfd.fd = 5 ; myfd.events = POLLIN | POLLOUT; int poll (struct pollfd *fds, nfds_t nfds, int timeout) ;- 参数: - fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合 - nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1 - timeout : 阻塞时长 0 : 不阻塞 -1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞 >0 : 阻塞的时长 - 返回值: -1 : 失败 >0 (n) : 成功,n表示检测到集合中有n个文件描述符发生变化
poll实现多路转接 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <poll.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_port = htons(9999 ); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); struct pollfd fds [1024]; for (int i = 0 ; i < 1024 ; i++) { fds[i].fd = -1 ; fds[i].events = POLLIN; } fds[0 ].fd = lfd; int nfds = 0 ; while (1 ) { int ret = poll(fds, nfds + 1 , -1 ); if (ret == -1 ) { perror("poll" ); exit (-1 ); } else if (ret == 0 ) { continue ; } else if (ret > 0 ) { if (fds[0 ].revents & POLLIN) { struct sockaddr_in cliaddr; int len = sizeof (cliaddr); int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len); for (int i = 1 ; i < 1024 ; i++) { if (fds[i].fd == -1 ) { fds[i].fd = cfd; fds[i].events = POLLIN; break ; } } nfds = nfds > cfd ? nfds : cfd; } for (int i = 1 ; i <= nfds; i++) { if (fds[i].revents & POLLIN) { char buf[1024 ] = {0 }; int len = read(fds[i].fd, buf, sizeof (buf)); if (len == -1 ) { perror("read" ); exit (-1 ); } else if (len == 0 ) { printf ("client closed...\n" ); close(fds[i].fd); fds[i].fd = -1 ; } else if (len > 0 ) { printf ("read buf = %s\n" , buf); write(fds[i].fd, buf, strlen (buf) + 1 ); } } } } } close(lfd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int fd = socket(PF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror("socket" ); return -1 ; } struct sockaddr_in seraddr ; inet_pton(AF_INET, "127.0.0.1" , &seraddr.sin_addr.s_addr); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(9999 ); int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret == -1 ){ perror("connect" ); return -1 ; } int num = 0 ; while (1 ) { char sendBuf[1024 ] = {0 }; sprintf (sendBuf, "send data %d" , num++); write(fd, sendBuf, strlen (sendBuf) + 1 ); int len = read(fd, sendBuf, sizeof (sendBuf)); if (len == -1 ) { perror("read" ); return -1 ; }else if (len > 0 ) { printf ("read buf = %s\n" , sendBuf); } else { printf ("服务器已经断开连接...\n" ); break ; } sleep(1 ); } close(fd); return 0 ; }
epoll
直接在内核操作,内核用rbtree比较快,事件驱动,红黑树节点上注册有回调函数,事件到来后执行回调函数
去掉拷贝到内核的开销,并且能告知哪些文件描述符发送改变,而不只是个数
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 #include <sys/epoll.h> 测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向 链表)。 int epoll_create (int size) ; - 参数: size : 目前没有意义了。随便写一个数,必须大于0 - 返回值: -1 : 失败 > 0 : 文件描述符 struct epoll_event { uint32_t events; epoll_data_t data; }; 常见的Epoll检测事件: - EPOLLIN - EPOLLOUT - EPOLLERR typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ; int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) ; - 参数: - epfd : epoll实例对应的文件描述符 - op : 要进行什么操作 EPOLL_CTL_ADD: 添加 EPOLL_CTL_MOD: 修改 EPOLL_CTL_DEL: 删除 - fd : 要检测的文件描述符 - event : 检测文件描述符什么事情 int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout) ; - 参数: - epfd : epoll实例对应的文件描述符 - events : 传出参数,保存了发送了变化的文件描述符的信息 - maxevents : 第二个参数结构体数组的大小 - timeout : 阻塞时间 - 0 : 不阻塞 - -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞 - > 0 : 阻塞的时长(毫秒) - 返回值: - 成功,返回发送变化的文件描述符的个数 > 0 - 失败 -1
epoll实现多路转接 建立epoll实例,添加EPOLLIN,用epoll_wait返回所有事件,可以得到所有事件; 然后按照事件类型逐个处理,把新连接进来的客户端添加到epoll实例中监听,把处理好的事件从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 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/epoll.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_port = htons(9999 ); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); int epfd = epoll_create(100 ); struct epoll_event epev ; epev.events = EPOLLIN; epev.data.fd = lfd; epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev); struct epoll_event epevs [1024]; while (1 ){ int ret = epoll_wait(epfd,epevs,1024 ,-1 ); if (ret == -1 ){ perror("epoll_wait" ); exit (-1 ); } printf ("ret = %d\n" ,ret); for (int i = 0 ; i < ret; i++){ int curfd =epevs[i].data.fd; if (curfd == lfd){ struct sockaddr_in cliaddr ; int len = sizeof (cliaddr); int cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len); epev.events = EPOLLIN; epev.data.fd = cfd; epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev); }else { if (epevs[i].events & EPOLLOUT){ continue ; } char buf[1024 ] = {0 }; int len = read(curfd,buf,sizeof (buf)); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL ); close(curfd); }else if (len > 0 ){ printf ("read buf = %s\n" ,buf); write(curfd,buf,strlen (buf)+1 ); } } } } close(epfd); close(lfd); }
Epoll的工作模式
LT 模式 (水平触发) 假设委托内核检测读事件 -> 检测fd的读缓冲区 读缓冲区有数据 - > epoll检测到了会给用户通知 a.用户不读数据,数据一直在缓冲区,epoll 会一直通知 b.用户只读了一部分数据,epoll会通知 c.缓冲区的数据读完了,不通知
LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这 种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操 作。如果你不作任何操作,内核还是会继续通知你的。
ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述 符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪, 并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述 符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成 未就绪),内核不会发送更多的通知(only once)。 ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写 操作把处理多个文件描述符的任务饿死。
1 2 3 4 5 6 7 8 9 struct epoll_event { uint32_t events; epoll_data_t data; }; 常见的Epoll检测事件: - EPOLLIN - EPOLLOUT - EPOLLERR - EPOLLET
水平触发 - 默认 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/epoll.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_port = htons(9999 ); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); int epfd = epoll_create(100 ); struct epoll_event epev ; epev.events = EPOLLIN; epev.data.fd = lfd; epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev); struct epoll_event epevs [1024]; while (1 ){ int ret = epoll_wait(epfd,epevs,1024 ,-1 ); if (ret == -1 ){ perror("epoll_wait" ); exit (-1 ); } printf ("ret = %d\n" ,ret); for (int i = 0 ; i < ret; i++){ int curfd =epevs[i].data.fd; if (curfd == lfd){ struct sockaddr_in cliaddr ; int len = sizeof (cliaddr); int cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len); epev.events = EPOLLIN; epev.data.fd = cfd; epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev); }else { if (epevs[i].events & EPOLLOUT){ continue ; } char buf[5 ] = {0 }; int len = read(curfd,buf,sizeof (buf)); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL ); close(curfd); }else if (len > 0 ){ printf ("read buf = %s\n" ,buf); write(curfd,buf,strlen (buf)+1 ); } } } } close(epfd); close(lfd); }
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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int fd = socket(PF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror("socket" ); return -1 ; } struct sockaddr_in seraddr ; inet_pton(AF_INET, "127.0.0.1" , &seraddr.sin_addr.s_addr); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(9999 ); int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret == -1 ){ perror("connect" ); return -1 ; } int num = 0 ; while (1 ) { char sendBuf[5 ] = {0 }; fgets(sendBuf,sizeof (sendBuf),stdin ); write(fd, sendBuf, strlen (sendBuf) + 1 ); int len = read(fd, sendBuf, sizeof (sendBuf)); if (len == -1 ) { perror("read" ); return -1 ; }else if (len > 0 ) { printf ("read buf = %s\n" , sendBuf); } else { printf ("服务器已经断开连接...\n" ); break ; } } close(fd); return 0 ; }
边沿触发 - EPOLLET 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 #include <stdio.h> #include <arpa/inet.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_port = htons(9999 ); saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); int epfd = epoll_create(100 ); struct epoll_event epev ; epev.events = EPOLLIN; epev.data.fd = lfd; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev); struct epoll_event epevs [1024]; while (1 ) { int ret = epoll_wait(epfd, epevs, 1024 , -1 ); if (ret == -1 ) { perror("epoll_wait" ); exit (-1 ); } printf ("ret = %d\n" , ret); for (int i = 0 ; i < ret; i++) { int curfd = epevs[i].data.fd; if (curfd == lfd) { struct sockaddr_in cliaddr ; int len = sizeof (cliaddr); int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len); int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); epev.events = EPOLLIN | EPOLLET; epev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev); } else { if (epevs[i].events & EPOLLOUT) { continue ; } char buf[5 ]; int len = 0 ; while ( (len = read(curfd, buf, sizeof (buf))) > 0 ) { write(STDOUT_FILENO, buf, len); write(curfd, buf, len); } if (len == 0 ) { printf ("client closed...." ); }else if (len == -1 ) { if (errno == EAGAIN) { printf ("data over....." ); }else { perror("read" ); exit (-1 ); } } } } } close(lfd); close(epfd); 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 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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main () { int fd = socket(PF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror("socket" ); return -1 ; } struct sockaddr_in seraddr ; inet_pton(AF_INET, "127.0.0.1" , &seraddr.sin_addr.s_addr); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(9999 ); int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret == -1 ){ perror("connect" ); return -1 ; } int num = 0 ; while (1 ) { char sendBuf[1024 ] = {0 }; fgets(sendBuf, sizeof (sendBuf), stdin ); write(fd, sendBuf, strlen (sendBuf) + 1 ); int len = read(fd, sendBuf, sizeof (sendBuf)); if (len == -1 ) { perror("read" ); return -1 ; }else if (len > 0 ) { printf ("read buf = %s\n" , sendBuf); } else { printf ("服务器已经断开连接...\n" ); break ; } } close(fd); return 0 ; }
4.7 UDP通信 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <sys/types.h> #include <sys/socket.h> ssize_t sendto (int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) ; - 参数: - sockfd : 通信的fd - buf : 要发送的数据 - len : 发送数据的长度 - flags : 0 - dest_addr : 通信的另外一端的地址信息 - addrlen : 地址的内存大小 ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) ; - 参数: - sockfd : 通信的fd - buf : 接收数据的数组 - len : 数组的大小 - flags : 0 - src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL - addrlen : 地址的内存大小
UDP实现流程 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET,SOCK_DGRAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(9999 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd,(struct sockaddr*)&addr,sizeof (addr)); if (ret == -1 ){ perror("bind" ); exit (-1 ); } while (1 ){ char recvbuf[128 ]; char ipbuf[16 ]; struct sockaddr_in cliaddr ; int len = sizeof (cliaddr); recvfrom(fd,recvbuf,sizeof (recvbuf),0 ,(struct sockaddr*)&cliaddr,&len); printf ("client IP: %s,Port: %d\n" , inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ipbuf,sizeof (ipbuf)), ntohs(cliaddr.sin_port)); printf ("client say: %s\n" ,recvbuf); sendto(fd,recvbuf,strlen (recvbuf)+1 ,0 ,(struct sockaddr*)&cliaddr,len); } close(fd); 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET,SOCK_DGRAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999 ); inet_pton(AF_INET,"127.0.0.1" ,&saddr.sin_addr.s_addr); int num = 0 ; while (1 ){ char sendBuf[128 ]; sprintf (sendBuf,"hello,i am client %d\n" ,num++); sendto(fd,sendBuf,strlen (sendBuf)+1 ,0 ,(struct sockaddr*)&saddr,sizeof (saddr)); recvfrom(fd,sendBuf,sizeof (sendBuf),0 ,NULL ,NULL ); printf ("server say: %s\n" ,sendBuf); sleep(1 ); } close(fd); return 0 ; }
广播 向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广 播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。 a.只能在局域网中使用。 b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息
1 2 3 4 5 6 7 8 9 int setsockopt (int sockfd, int level, int optname,const void *optval, socklen_t optlen) ; - sockfd : 文件描述符 - level : SOL_SOCKET - optname : SO_BROADCAST - optval : int 类型的值,为1 表示允许广播 - optlen : optval的大小
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 <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET,SOCK_DGRAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } int op = 1 ; setsockopt(fd,SOL_SOCKET,SO_BROADCAST,&op,sizeof (op)); struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(9999 ); inet_pton(AF_INET,"192.168.31.255" ,&cliaddr.sin_addr.s_addr); int num=0 ; while (1 ){ char sendbuf[128 ]; sprintf (sendbuf,"hello,client...%d\n" ,num++); sendto(fd,sendbuf,strlen (sendbuf)+1 ,0 ,(struct sockaddr*)&cliaddr,sizeof (cliaddr)); printf ("广播的数据: %d\n" ,num); sleep(1 ); } close(fd); 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET,SOCK_DGRAM,0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(9999 ); cliaddr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd,(struct sockaddr*)&cliaddr,sizeof (cliaddr)); if (ret == -1 ){ perror("bind" ); exit (-1 ); } while (1 ){ char buf[128 ]; recvfrom(fd,buf,sizeof (buf),0 ,NULL ,NULL ); printf ("server say : %s\n" , buf); } close(fd); return 0 ; }
组播(多播) 单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。 单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方 案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上 的接口接收 。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网 使用。 a.组播既可以用于局域网,也可以用于广域网 b.客户端需要加入多播组,才能接收到多播的数据
组播地址: IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 , 并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int setsockopt (int sockfd, int level, int optname,const void *optval, socklen_t optlen) ; - level : IPPROTO_IP - optname : IP_MULTICAST_IF - optval : struct in_addr // // 客户端加入到多播组: - level : IPPROTO_IP - optname : IP_ADD_MEMBERSHIP - optval : struct ip_mreq struct ip_mreq { struct in_addr imr_multiaddr ; struct in_addr imr_interface ; }; typedef uint32_t in_addr_t ;struct in_addr { in_addr_t s_addr; };
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ) { perror("socket" ); exit (-1 ); } struct in_addr imr_multiaddr ; inet_pton(AF_INET, "239.0.0.10" , &imr_multiaddr.s_addr); setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof (imr_multiaddr)); struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(9999 ); inet_pton(AF_INET, "239.0.0.10" , &cliaddr.sin_addr.s_addr); int num = 0 ; while (1 ) { char sendBuf[128 ]; sprintf (sendBuf, "hello, client....%d\n" , num++); sendto(fd, sendBuf, strlen (sendBuf) + 1 , 0 , (struct sockaddr *)&cliaddr, sizeof (cliaddr)); printf ("组播的数据:%s\n" , sendBuf); sleep(1 ); } close(fd); 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 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ) { perror("socket" ); exit (-1 ); } struct in_addr in ; struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(9999 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr *)&addr, sizeof (addr)); if (ret == -1 ) { perror("bind" ); exit (-1 ); } struct ip_mreq op ; inet_pton(AF_INET, "239.0.0.10" , &op.imr_multiaddr.s_addr); op.imr_interface.s_addr = INADDR_ANY; setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof (op)); while (1 ) { char buf[128 ]; int num = recvfrom(fd, buf, sizeof (buf), 0 , NULL , NULL ); printf ("server say : %s\n" , buf); } close(fd); return 0 ; }
4.8 本地套接字
本地套接字的作用:本地的进程间通信 有关系的进程间的通信 没有关系的进程间的通信 本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。
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 #define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; char sun_path[UNIX_PATH_MAX]; }; 1. 创建监听的套接字 int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0 ); 2. 监听的套接字绑定本地的套接字文件 -> server端 struct sockaddr_un addr ; bind(lfd, addr, len); 3. 监听 listen(lfd, 100 ); 4. 等待并接受连接请求 struct sockaddr_un cliaddr ; int cfd = accept(lfd, &cliaddr, len); 5. 通信 接收数据:read/recv 发送数据:write/send 6. 关闭连接 close(); 1. 创建通信的套接字 int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0 ); 2. 监听的套接字绑定本地的IP 端口 struct sockaddr_un addr ; bind(lfd, addr, len); 3. 连接服务器 struct sockaddr_un serveraddr ; connect(fd, &serveraddr, sizeof (serveraddr)); 4. 通信 接收数据:read/recv 发送数据:write/send 5. 关闭连接 close();
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 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/un.h> int main () { unlink("server.sock" ); int lfd = socket(AF_LOCAL, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("socket" ); exit (-1 ); } struct sockaddr_un addr ; addr.sun_family = AF_LOCAL; strcpy (addr.sun_path, "server.sock" ); int ret = bind(lfd, (struct sockaddr *)&addr, sizeof (addr)); if (ret == -1 ) { perror("bind" ); exit (-1 ); } ret = listen(lfd, 100 ); if (ret == -1 ) { perror("listen" ); exit (-1 ); } struct sockaddr_un cliaddr ; int len = sizeof (cliaddr); int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len); if (cfd == -1 ) { perror("accept" ); exit (-1 ); } printf ("client socket filename: %s\n" , cliaddr.sun_path); while (1 ) { char buf[128 ]; int len = recv(cfd, buf, sizeof (buf), 0 ); if (len == -1 ) { perror("recv" ); exit (-1 ); } else if (len == 0 ) { printf ("client closed....\n" ); break ; } else if (len > 0 ) { printf ("client say : %s\n" , buf); send(cfd, buf, len, 0 ); } } close(cfd); close(lfd); 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 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 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <arpa/inet.h> #include <sys/un.h> int main () { unlink("client.sock" ); int cfd = socket(AF_LOCAL, SOCK_STREAM, 0 ); if (cfd == -1 ) { perror("socket" ); exit (-1 ); } struct sockaddr_un addr ; addr.sun_family = AF_LOCAL; strcpy (addr.sun_path, "client.sock" ); int ret = bind(cfd, (struct sockaddr *)&addr, sizeof (addr)); if (ret == -1 ) { perror("bind" ); exit (-1 ); } struct sockaddr_un seraddr ; seraddr.sun_family = AF_LOCAL; strcpy (seraddr.sun_path, "server.sock" ); ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret == -1 ) { perror("connect" ); exit (-1 ); } int num = 0 ; while (1 ) { char buf[128 ]; sprintf (buf, "hello, i am client %d\n" , num++); send(cfd, buf, strlen (buf) + 1 , 0 ); printf ("client say : %s\n" , buf); int len = recv(cfd, buf, sizeof (buf), 0 ); if (len == -1 ) { perror("recv" ); exit (-1 ); } else if (len == 0 ) { printf ("server closed....\n" ); break ; } else if (len > 0 ) { printf ("server say : %s\n" , buf); } sleep(1 ); } close(cfd); return 0 ; }
5.WebServer项目 5.1 阻塞非阻塞、同步异步
典型的一次IO的两个阶段是什么?数据就绪 和 数据读写
数据就绪:根据系统IO操作(现在只考虑网络IO)的就绪状态 阻塞 非阻塞
数据读写:根据应用程序和内核的交互方式 同步 异步
陈硕:在处理 IO 的时候,阻塞和非阻塞都是同步 IO,只有使用了特殊的 API 才是异步 IO
一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪” 和 “数据读写”,数据就绪阶段分为 阻塞和非阻塞,表现得结果就是,阻塞当前线程或是直接返回。 同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是 由请求方A自己来完成 的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络IO接口时 (或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以 处理其它逻辑了,当B监听到事件处理 完成后,会用事先约定好的通知方式,通知A处理结果。
同步阻塞 同步非阻塞 异步阻塞 异步非阻塞
5.2 Unix/Linux上的五种IO模型 a.阻塞 blocking 调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必 须等这个函数返回才能进行下一步动作
有等待 阻塞 2. 有数据拷贝 同步
b.非阻塞 non-blocking(NIO) 非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调 用总是立即返回 ,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两 种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN 。
c.IO复用(IO multiplexing) Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是 这些函数可以同时(非)阻塞地处理多个IO操作 。而且可以同时对多个读操作、写操作的IO函数进行检测 。直到有数 据可读或可写时,才真正调用IO操作函数。//主要不是处理高并发,而是同时处理多个IO的优点
d.信号驱动(signal-driven) Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进 程收到SIGIO 信号,然后处理 IO 事件
内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需 要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率
//从内核空间拷贝到用户空间还是需要拷贝,也就是同步操作,不常用
e.异步(asynchronous) Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方 式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct aiocb { int aio_fildes; int aio_lio_opcode; int aio_reqprio; volatile void *aio_buf; size_t aio_nbytes; struct sigevent aio_sigevent ; struct aiocb *__next_prio ; int __abs_prio; int __policy; int __error_code; __ssize_t __return_value; #ifndef __USE_FILE_OFFSET64 __off_t aio_offset; char __pad[sizeof (__off64_t ) - sizeof (__off_t )]; #else __off64_t aio_offset; #endif char __glibc_reserved[32 ]; };
5.3 Web Server网页服务器 网页服务器简介 一个 Web Server 就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。其主 要功能是通过 HTTP 协议与客户端(通常是浏览器(Browser))进行通信,来接收,存储,处理来自 客户端的 HTTP 请求,并对其请求做出 HTTP 响应,返回给客户端其请求的内容(文件、网页等)或返 回一个 Error 信息。
通常用户使用 Web 浏览器与相应服务器进行通信。在浏览器中键入“域名”或“IP地址:端口号”,浏览器则 先将你的域名解析成相应的 IP 地址或者直接根据你的IP地址向对应的 Web 服务器发送一个 HTTP 请 求。这一过程首先要通过 TCP 协议的三次握手建立与目标 Web 服务器的连接,然后 HTTP 协议生成针 对目标 Web 服务器的 HTTP 请求报文,通过 TCP、IP 等协议发送到目标 Web 服务器上。
HTTP 概述 超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求 - 响应协议,它通常运行在 TCP 之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的 头以 ASCII 形式给出;而消息内容则具有一个类似 MIME 的格式。HTTP是万维网的数据通信的基础。
HTTP 是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80,HTTPS是443 )。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如 HTML 文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。尽管 TCP/IP 协议是互联网上最流行的应用,HTTP 协议中,并没有规定必须使用它或它支持的层。事实上,HTTP可以在任何互联网协议上,或其他网络上实现 。HTTP 假定其下层协议提供可靠的传输。因此,任何能够提供这种保证的协议都可以被其使用。因此也就是其在 TCP/IP 协议族使用 TCP 作为其传输层 。 通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的 TCP 连接。HTTP 服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如”HTTP/1.1 200 OK”,以及返回的内容,如请求的文件、错误消息、或者其它信息。
工作原理 HTTP 协议定义 Web 客户端如何从 Web 服务器请求 Web 页面,以及服务器如何把 Web 页面传送给客 户端。HTTP 协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方 法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版 本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:
客户端连接到 Web 服务器 一个HTTP客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80 )建立一个 TCP 套接 字连接。例如,http://www.baidu.com。(URL)
发送 HTTP 请求 通过 TCP 套接字,客户端向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行、请求 头部、空行和请求数据 4 部分组成。
服务器接受请求并返回 HTTP 响应 Web 服务器解析请求,定位请求资源。服务器将资源复本写到 TCP 套接字,由客户端读取。一个 响应由状态行、响应头部、空行和响应数据 4 部分组成。
释放连接 TCP 连接 若 connection 模式为 close,则服务器主动关闭 TCP连接,客户端被动关闭连接,释放 TCP 连 接;若connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
客户端浏览器解析 HTML 内容 客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应 头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据 HTML 的语法对其进行格式化,并在浏览器窗口中显示。
例如:在浏览器地址栏键入URL,按下回车之后会经历以下流程:
浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立 TCP 连接;
浏览器发出读取文件( URL 中域名后面部分对应的文件)的 HTTP 请求,该请求报文作为 TCP 三 次握手的第三个报文的数据发送给服务器;
服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器;
释放 TCP 连接;
浏览器将该 HTML 文本并显示内容。
HTTP 协议是基于 TCP/IP 协议之上的应用层协议,基于 请求-响应 的模式。HTTP 协议规定,请求从客 户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端 在没有接收到请求之前不会发送响应
请求报文 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET / HTTP/1.1 Host : www.baidu.comUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8Accept-Language : zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding : gzip, deflate, brConnection : keep-aliveCookie : BAIDUID=6729CB682DADC2CF738F533E35162D98:FG=1; BIDUPSID=6729CB682DADC2CFE015A8099199557E; PSTM=1614320692; BD_UPN=13314752; BDORZ=FFFB88E999055A3F8A630C64834BD6D0; __yjs_duid=1_d05d52b14af4a339210722080a668ec21614320694782; BD_HOME=1; H_PS_PSSID=33514_33257_33273_31660_33570_26350; BA_HECTOR=8h2001alag0lag85nk1g3hcm60q Upgrade-Insecure-Requests : 1Cache-Control : max-age=
响应报文 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 HTTP/1.1 200 OKBdpagetype : 1Bdqid : 0xf3c9743300024ee4Cache-Control : privateConnection : keep-aliveContent-Encoding : gzipContent-Type : text/html;charset=utf-8Date : Fri, 26 Feb 2021 08:44:35 GMTExpires : Fri, 26 Feb 2021 08:44:35 GMTServer : BWS/1.1Set-Cookie : BDSVRTM=13; path=/Set-Cookie : BD_HOME=1; path=/Set-Cookie : H_PS_PSSID=33514_33257_33273_31660_33570_26350; path=/; domain=.baidu.comStrict-Transport-Security : max-age=172800Traceid : 1614329075128412289017566699583927635684X-Ua-Compatible : IE=Edge,chrome=1Transfer-Encoding : chunked
http状态码 所有HTTP响应的第一行都是状态行,依次是当前HTTP版本号,3位数字组成的状态代码,以及描述状态 的短语,彼此由空格分隔。 状态代码的第一个数字代表当前响应的类型: 1xx消息——请求已被服务器接收,继续处理 2xx成功——请求已成功被服务器接收、理解、并接受 3xx重定向——需要后续操作才能完成这一请求 4xx请求错误——请求含有词法错误或者无法被执行 5xx服务器错误——服务器在处理某个正确请求时发生错误 虽然 RFC 2616 中已经推荐了描述状态的短语,例如”200 OK”,”404 Not Found”,但是WEB开发者仍 然能够自行决定采用何种短语,用以显示本地化的状态描述或者自定义信息。
更多状态码:https://baike.baidu.com/item/HTTP%E7%8A%B6%E6%80%81%E7%A0%81/5053660?fr=aladdin
事件处理模式 服务器程序通常需要处理三类事件:I/O 事件、信号及定时事件。有两种高效的事件处理模式:Reactor 和 Proactor,同步 I/O 模型通常用于实现 Reactor 模式,异步 I/O 模型通常用于实现 Proactor 模式。
Reactor模式 要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作 线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做 任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是
主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
主线程调用 epoll_wait 等待 socket 上有数据可读。
当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
当主线程调用 epoll_wait 等待 socket 可写。
当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。
Proactor模式 Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻 辑。使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程是:
主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置, 以及读操作完成时如何通知应用程序(这里以信号为例)。
主线程继续处理其他逻辑。
当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据 已经可用。
应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求 后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以 及写操作完成时如何通知应用程序。
主线程继续处理其他逻辑。
当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据 已经发送完毕。
应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
模拟 Proactor 模式 使用同步 I/O 方式模拟出 Proactor 模式。原理是:主线程执行数据读写操作,读写完成之后,主线程向 工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下 来要做的只是对读写的结果进行逻辑处理。 使用同步 I/O 模型(以 epoll_wait为例)模拟出的 Proactor 模式的工作流程如下:
主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
主线程调用 epoll_wait 等待 socket 上有数据可读。
当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更 多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事 件表中注册 socket 上的写就绪事件。
主线程调用 epoll_wait 等待 socket 可写。
当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。
线程池 线程池是由服务器预先创建的一组子线程,线程池中的线程数量应该和 CPU 数量差不多 。线程池中的所 有子线程都运行着相同的代码。当有新的任务到来时,主线程将通过某种方式选择线程池中的某一个子 线程来为之服务。相比与动态的创建子线程,选择一个已经存在的子线程的代价显然要小得多。至于主 线程选择哪个子线程来为新任务服务,则有多种方式:
主线程使用某种算法来主动选择子线程。最简单、最常用的算法是随机算法和 Round Robin(轮流 选取)算法,但更优秀、更智能的算法将使任务在各个工作线程中更均匀地分配 ,从而减轻服务器 的整体压力。
主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在该工作队列上 。当有新的任 务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线 程将获得新任务的”接管权“,它可以从工作队列中取出任务并执行之,而其他子线程将继续睡眠在 工作队列上
线程池中的线程数量最直接的限制因素是中央处理器(CPU)的处理器(processors/cores)的数量 N :如果你的CPU是4-cores的,对于CPU密集型的任务(如视频剪辑等消耗CPU计算资源的任务)来 说,那线程池中的线程数量最好也设置为4(或者+1防止其他因素造成的线程阻塞);对于IO密集 型的任务,一般要多于CPU的核数,因为线程间竞争的不是CPU的计算资源而是IO,IO的处理一 般较慢,多于cores数的线程将为CPU争取更多的任务,不至在线程处理IO的过程造成CPU空闲导 致资源浪费。
设置线程池目的:
空间换时间,浪费服务器的硬件资源,换取运行效率。
池是一组资源的集合,这组资源在服务器启动之初就被完全创建好并初始化,这称为静态资源。
当服务器进入正式运行阶段,开始处理客户请求的时候,如果它需要相关的资源,可以直接从池中 获取,无需动态分配。
当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用释放资源
EPOLLONESHOT事件 即使可以使用 ET 模式,一个socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个 问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该 socket 上又有新数据可读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于 是就出现了两个线程同时操作一个 socket 的局面。一个socket连接在任一时刻都只被一个线程处理 ,可 以使用 epoll 的 EPOLLONESHOT 事件实现。 对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异 常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事 件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的。但反过来思 考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕, 该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进 而让其他工作线程有机会继续处理这个 socket。
有限状态机 逻辑单元内部的一种高效编程方法:有限状态机(finite state machine)。 有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态 ,服务器可以 根据它来编写相应的处理逻辑。如下是一种状态独立的有限状态机:
1 2 3 4 5 6 7 8 9 10 11 12 13 STATE_MACHINE( Package _pack ) { PackageType _type = _pack.GetType(); switch ( _type ) { case type_A: process_package_A( _pack ); break ; case type_B: process_package_B( _pack ); break ; } }
这是一个简单的有限状态机,只不过该状态机的每个状态都是相互独立的,即状态之间没有相互转移。 状态之间的转移是需要状态机内部驱动,如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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: process_package_state_B( _pack ); cur_State = type_C; break ; } }
该状态机包含三种状态:type_A、type_B 和 type_C,其中 type_A 是状态机的开始状态,type_C 是状 态机的结束状态。状态机的当前状态记录在 cur_State 变量中。在一趟循环过程中,状态机先通过 getNewPackage 方法获得一个新的数据包,然后根据 cur_State 变量的值判断如何处理该数据包。数据 包处理完之后,状态机通过给 cur_State 变量传递目标状态值来实现状态转移。那么当状态机进入下一 趟循环时,它将执行新的状态对应的逻辑。
服务器压力测试 Webbench 是 Linux 上一款知名的、优秀的 web 性能压力测试工具。它是由Lionbridge公司开发
测试处在相同硬件上,不同服务的性能以及不同硬件上同一个服务的运行状况。
展示服务器的两项内容:每秒钟响应请求数和每秒钟传输数据量。
基本原理:Webbench 首先 fork 出多个子进程,每个子进程都循环做 web 访问测试。子进程把访问的 结果通过pipe 告诉父进程,父进程做最终的统计结果。
Ubuntu一些小操作 配置类 文本文件上传 文本文件的换行符
Windows : \r\n
Linux : \n
可以在 Notepad ++ 里观察到此区别
视图 | 显示符号 | 显示行尾符
修改格式 换行符的转换:
编辑 | 文档格式转换 | 转换为 UNIX格式
注意:只有在编辑 SHELL 脚本时,才需要转换
其他格式的文件一般都不需要转换,如*.xml, *.java
演示 :Shell脚本的编辑 。。
用 Notepad++打开编辑 mytest.sh
转成 Unix格式 \n
上传至Linux
chmod +x mytest.sh
运行 ./mytest.sh
命令类 1. 归档 tar , 即 tape archive 档案打包
创建档案包
tar -cvf example.tar example
其中,
c , 表示 create 创建档案
v , 表示 verbose 显示详情
f , 表示 file
也可以多个目录打包 tar -cvf xxx.tar file1 file2 file3
还原档案包
tar -xvf example.tar
tar -xvf example.tar -C outdir
其中,-C 参数指定目标目录,默认解到当前目录下
2. 压缩解压 先前的tar格式并没有压缩,体积较大
并档并压缩
tar -zcvf example.tar.gz example
解压缩
tar -zxvf example.tar.gz
tar -zxvf example.tar.gz -C outdir
通常我们所见的,都是 *.tar.gz 这种格式
7z 1 2 3 4 5 sudo apt install p7zip-full # 压缩 7z a -t7z -r filename.7z ./* # 解压 7z x filename.7z -r -o./*
环境变量 定义环境变量
export OUTDIR=/opt/
显示环境变量
echo ${OUTDIR}
查看所有环境变量
printenv
查看当前进程 进程id:echo $$
终端设备:tty
找函数 man 2 xxx
或者 man xxx +tab键
查看网络相关信息的命令 netstat 参数:-a 所有的socket -p 显示正在使用socket的程序的名称 -n 直接使用IP地址,而不通过域名服务器
netstat -anp|grep xxxx 查看端口号占用信息
小知识 利用Core文件查看异常的信息
用ulimit -a查看Core文件允许产生的大小,一般是0;然后改为一定值 ulimit -c 1024
然后用-g调试编译.c文件
然后调试改文件,输入core-file core就能看到
这里系统版本不同,生成不出来Core文件
bt打印堆栈
Linux下的时间设置 时钟时间 = 阻塞时间 + 就绪时间 +运行时间;
其中:运行时间=用户CPU时间(用户的进程获得了CPU资源以后,在用户态执行的时间。)+系统CPU时间(用户进程获得了CPU资源以后,在内核态的执行时间。);
因为Linux是多任务操作系统,往往在执行一条命令时,系统还要处理其它任务
前后台进程 默认前台进程,阻塞
加上&改为后台进程,改为非阻塞 ,但是注意要去查看,并且kill -9:./sigprocmask&
段错误究竟是怎么发生的?段错误的复现为什么这么难? 段错误是个迷,有的人碰到过几次,有的人怎么也碰不到,这是由于神秘莫测的调度算法导致的。【潇潇_暮雨】小伙伴提出了,这是调用了不可重入的函数。《Linux/UNIX系统编程手册》第21.1.2节 对可重入函数进行了详细的解释,有兴趣的可以去翻一下。
可重入函数的意思是:函数由两条或多条线程调用时,即便是交叉执行,其效果也与各线程以未定义顺序依次调用时一致。通俗点讲,就是存在一个函数,A线程执行一半,B线程抢过CPU又来调用该函数,执行到1/4倍A线程抢回执行权。在这样不断来回执行中,不出问题的,就是可重入函数。多线程中每个线程都有自己的堆栈,所以如果函数中只用到局部变量肯定是可重入的,没问题的。但是更新了全局变量或静态数据结构的函数可能是不可重入的。假设某线程正在为一个链表结构添加一个新的链表项,而另外一个线程也视图更新同一链表。由于中间涉及多个指针,一旦另一线程中断这些步骤并修改了相同指针,结果就会产生混乱。但是并不是一定会出现,一定是A线程刚好在修改指针,另外一线程又去修改才会出现。这就是为什么该问题复现难度较高的原因。
作者在文中指出,将静态数据结构用于内部记账的函数也是不可重入的。其中最明显的例子就是stdio函数库成员(printf()、scanf()等),它们会为缓冲区I/O更新内部数据结构。所以,如果在捕捉信号处理函数中调用了printf(),而主程序又在调用printf()或其他stdio函数期间遭到了捕捉信号处理函数的中断,那么有时就会看到奇怪的输出,设置导致程序崩溃。虽然printf()不是异步信号安全函数,但却频频出现在各种示例中,是因为在展示对捕捉信号处理函数的调用,以及显示函数中相关变量的内容时,printf()都不失为一种简单而又便捷的方式。真正的应用程序应当避免使用该类函数。
printf函数会使用到一块缓冲区,这块缓冲区是使用malloc或类似函数分配的一块静态内存。所以它是不可重入函数
虚拟地址空间层次划分 从操作系统层级上看,虚拟地址空间主要分为两个部分内核区和用户区。
一、内核区
内核空间为内核保留,不允许应用程序读写该区域的内容 或直接调用内核代码定义的函数
内核总是驻留在内存中 ,是操作系统的一部分。
系统中所有进程 对应的虚拟地址空间的内核区 都会映射到同一块物理内存 上(系统内核只有一个 )
二、用户区
每个进程的虚拟地址空间都是从 0 地址开始的 ,我们在程序中打印的变量地址也其在虚拟地址空间中的地址,程序是无法直接访问物理内存的。虚拟地址空间中用户区地址范围是 0~3G(以 32 位系统的虚拟地址空间为例),里边分为多个区块。
各分区由低地址到高地址依次是:
保留区: 位于虚拟地址空间的最底部,未赋予物理地址 。任何对它的引用都是非法的,程序中的空指针(NULL)指向的就是这块内存地址 。
.text段: 代码段也称正文段或文本段 ,通常用于存放程序的执行代码 (即 CPU 执行的机器指令,二进制 ),代码段一般情况下是只读 的,这是对执行代码的一种保护机制。
.data段 : 数据段 通常用于存放程序中已初始化且初值不为 0 的全局变量和静态变量 。数据段属于**静态内存分配 (静态存储区)**,可读可写。
.bss段: 未初始化以及初始为 0 的全局变量和静态变量 ,操作系统会将这些未初始化变量初始化为 0 ;
堆(heap):用于存放进程运行时 动态分配的内存 。
堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
堆向高地址扩展 (即 “向上生长”),是不连续 的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历 。
内存映射区(mmap):**作为内存映射区**加载磁盘文件**,或者 加载程序运作过程中需要调用的动态库**。
栈(stack): 存储函数内部声明的非静态局部变量,函数参数,函数返回地址等信息 ,栈内存由编译器自动分配释放 。栈 和堆相反地址 “向下生长” ,分配的内存是连续 的。
命令行参数 :存储进程执行的时候传递给 main() 函数的参数,argc,argv []
环境变量 : 存储和进程相关的环境变量,比如:工作路径 ,进程所有者 等信息
C函数记录 1 2 3 4 5 6 7 8 9 10 int fprintf (FILE *stream, const char *format, ...) ;第一个参数:(buffer) 这个参数就是接收字符串的字符数组。其大小必须要大于所接收的字符串的大小,否则的话会有空间不够从而导致内存溢出的风险。(这里比较大小时还要考虑到字符串最后的 ‘\0’) 第二个参数:(format) 这个参数就是要传的字符串了。 其余参数: 剩下的参数其实算是对第二个参数format的补充,可有可无,视情况而定
1 2 3 #include <stdio.h> perror("xxx" ) 作用:调用系统或者库函数有错的时候,发出错误信息
sizeof 和strlen 有本质上的区别。sizeof 是C 语言的一种单目运算符,如++、–等,并不是函数,sizeof 的优先级为2 级,比/、% 等3 级运算符优先级高,sizeof以字节的形式给出操作数的存储空间的大小。而 strlen 是一个函数,是由 C 语言的标准库提供的。strlen 计算的 是字符串的长度。
1 2 3 4 5 6 7 8 sprintf int sprintf ( char *buffer, const char *format [, argument] … ) ; 跟 printf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出; 1. 可以用sprintf 来将其他类型转换字符串类想2. 可以生成字符串,拼接 格式等 sprintf (buf,"hello,%d\n" ,i);
可变参数 输入一串格式化的字符串,经过处理后可以将 %s %f %d等占位符替换为对应的数据;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 … 表示函数的参数个数可变,典型的如printf () 第一个参数是一个格式化字符串,后面是与格式化字符串中的代码相对应的不同类型的多个参数。 char * func (const char * format, ...) { va_list ap; char *res = NULL ; va_start(ap, format); res = func(format, ap); va_end(ap); return res ; } 1. int vsprintf (char *str, const char *format, va_list ap) ;函数与sprintf ()函数对应,只是在函数调用时,把上面的...对应的一个个变量用va_list调用所替代。在函数调用前ap要通过va_start()宏来动态获取。2. 结构体va_list用来存参数列表,
1 2 3 4 5 6 7 8 9 10 11 12 13 struct iovec //I /O vector ,与readv 和wirtev 操作相关的结构体#include < sys/uio.h>struct iovec { void *iov_base; size_t iov_len; }; 成员iov_base指向一个缓冲区,这个缓冲区是存放readv所接收的数据或是writev将要发送的数据。 成员iov_len确定了接收的最大长度以及实际写入的长度。 readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write) HTTP响应的时候配合writev(),写入响应头和体 writev(m_sockfd,m_iv,m_iv_count);