一些OS和Linux的前置姿势
Linux & OS
静态库和动态库
静态库
steps for build a static link library:
gcc -c a.c b.c xxx.c ...
ar rcs libxxx.a a.o b.o ... -I ./include_dir
complie the main code (don’t forget to add -I and -L -L ./lib_dir -lxxx
)
动态库
steps for build a dynamic link library:
gcc -c -fpic a.c b.c xxx.c ...
(-fpic代表生成与位置无关的代码)
gcc -shared a.o b.o ... -o libxxx.so
complie the main code (don’t forget to add -I and -L)
add lib path to env:
user: add LD_LIBRARY_PATH=$LD_LIBRARY_PATH:absolute_lib_path
to ~/.bashrc
and then source ~/.bashrc
root: add LD_LIBRARY_PATH=$LD_LIBRARY_PATH:absolute_lib_path
to /etc/profile
and then source /etc/profile
Makefile
基本操作
函数
.PHONY:
后跟的名称为伪文件(即:不是一个文件),用于防止和自己定义的文件名冲突而使规则无效化(一直显示最新)
Makefile的特点:
举例:一个文件树如下,其中a.c, b.c
定义了头文件head.h
声明的函数,而main.c
使用了a.c, b.c
定义的函数。此处我想把a.c, b.c
打包成静态库,并编译main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| LIB_DIR=./lib LIB_NAME=calc INCLUDE_DIR=./include SRC_DIR=./src TARGET=app MAIN=main.c DEPENDENCE=$(MAIN) $(LIB_DIR)/lib$(LIB_NAME).a $(INCLUDE_DIR) DEP_C=$(wildcard ./src/*.c) DEP_O=$(patsubst %.c, %.o, $(DEP_C))
$(TARGET): $(DEPENDENCE) $(CC) $(MAIN) -o $@ -I $(INCLUDE_DIR) -L $(LIB_DIR) -l$(LIB_NAME)
$(LIB_DIR)/lib$(LIB_NAME).a: $(DEP_O) ar rcs $@ $^
$(SRC_DIR)/%.o: $(SRC_DIR)/%.c $(INCLUDE_DIR) $(CC) -c $< -o $@ -I $(INCLUDE_DIR)
.PHONY: clean clean: rm -f $(DEP_O)
|
GDB
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
| list/l list/l 函数名: 行数 list/l 文件名: 行数 show list/listsize set list/listsize
break/b 文件名: 行号 break/b 文件名: 函数名 info/i b/break delete/d id disable/enable id break 行号 if i==5
start run c/continue n/next s/step finish until p/print 变量名 ptype 变量名 display 变量名 i/info display undisplay id set var 变量名=变量值
set follow-fork-mode prent/child set detach-on-fork on/off info inferiors inferior id detach inferiors id
|
虚拟地址空间与文件IO
32位机:3G用户态,1G内核态
64位机:281T($2^{48}$)用户态,基本可认为无穷大
文件描述符:保存在内核区PCB(进程控制块)
文件描述符表:一个数组(默认大小1024),保存每个打开的文件描述符信息
open和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
| #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>
int open(const char *pathname, int flags); 参数: - pathname: 要打开的文件路径 - flags: 对文件的操作权限和其他设置 O_RDONLY, O_WRONLY, ORDWR 返回值: 一个新的文件描述符,若失败则返回-1
errno: 属于Linux系统函数库的全局变量,记录最近的错误号
#include <stdio.h> void perror(const char *s); s参数: 用户描述,比如hello,最终输出的是 hello: xxx(实际的错误描述)
int open(const char *pathname, int flags, mode_t mode); 参数: - pathname - flags(int 4个字节,32位标志位,支持按位或) - 必选: O_RDONLY, O_WRONLY, O_RDWR - 可选: ..., O_CREAT 文件不存在,创建新文件 - mode: 8进制数,表示创建出的新文件的操作权限 最终的权限是:mode & ~umask 例如: 0777 & (~0022) ------------- 0777 -> 0 111 111 111 & 0755 -> 0 111 101 101 -------------------------- 0755 <- 0 111 101 101 umask的作用就是抹去某些权限
#include <unistd.h> int close(int fildes);
|
open和close
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main(){
int fd = open("create.txt", O_RDWR | O_CREAT, 0777); if(fd == -1){ perror("open"); } close(fd);
return 0; }
|
read和write
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <unistd.h> ssize_t read(int fd, void *buf, size_t count); 参数: - fd:文件描述符,open得到,通过fd操作某文件 - buf:需要读取数据存放的地方,数组的地址 - count:指定数组的大小 返回值: - 成功: >0: 返回实际的读取到的字节数 =0: 文件已经读取完了 - 失败: -1,并设置errno
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); 参数: - fd:文件描述符 - buf:往磁盘写入的数据,数组 - count:要写入的数据的实际大小 返回值: - 成功:返回实际写入的字节数 - 失败:返回-1,并设置errno
|
read和write
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
| #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(){
int srcfd = open("english.txt", O_RDONLY); if(srcfd == -1){ perror("open"); return -1; }
int destfd = open("cpy.txt", O_WRONLY | O_CREAT, 0664); if(destfd == -1){ perror("open"); return -1; }
char buf[1024] = {0}; int len = 0; while((len = read(srcfd, buf, sizeof buf))>0){ write(destfd, buf, len); }
close(destfd); close(srcfd);
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库fseek #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:文件描述符 - 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) lseek(fd, 100, SEEK_END);
|
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
| #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和lstat
| #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); 作用:获取一个文件相关的一些信息(可以获取软链接文件本身信息) 其他同上
|
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> #include <stdio.h> int main(){
struct stat statbuf;
int ret = stat("a.txt", &statbuf);
if(ret == -1){ perror("stat"); return -1; }
printf("size: %ld\n", statbuf.st_size);
return 0; }
|
Linux C语言实现`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
|
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.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; }
char tab[3] = {'x', 'w', 'r'}; for(int i=0;i<9;++i) perms[9-i] = ((st.st_mode>>i)&1) ? tab[i%3] : '-'; perms[10] = '\0';
int linkNum = st.st_nlink;
char *fileUser = getpwuid(st.st_uid)->pw_name;
char *fileGrp = getgrgid(st.st_gid)->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, chmod, chown, truncate)
| #include <unistd.h> int access(const char *pathname, int mode); 作用:判断某文件是否有某种权限,或是否存在 参数: pathname:文件名 mode: R_OK: 是否有读权限 W_OK: 判断是否有写权限 X_OK: 判断是否有执行权限 F_OK: 判断是否存在 返回值: 成功返回0, 失败返回-1
|
access
| #include <unistd.h> #include <stdio.h> int main(){
int ret = access("a.txt", F_OK); if(ret == -1){ perror("access"); return -1; }
printf("File exists!!!\n");
return 0; }
|
| #include <sys/stat.h> int chmod(const char *pathname, mode_t mode); 作用:修改文件权限 参数: - pathname:文件路径 - mode:需要修改的权限,8进制数 返回值:成功0,失败-1
|
chmod
| #include <sys/stat.h> #include <stdio.h> int main(){
int ret = chmod("a.txt", 0644); if(ret == -1){ perror("chmod"); return -1; } return 0; }
|
chown
| #include <unistd.h> #include <sys/types.h> int truncate(const char *path, off_t length); 作用:所见或扩展文件尺寸至指定大小 参数: - path:需要修改的文件路径 - length:需要的最终文件变成的大小 返回值: 成功返回0,失败-1
|
truncate
| #include <unistd.h> #include <sys/types.h> #include <stdio.h> int main(){
int ret = truncate("a.txt", 5); if(ret == -1){ perror("truncate"); return -1; } return 0; }
|
目录操作函数(mkdir, rmdir, rename, chdir, getcwd)
| #include <unistd.h> int chdir(const char *path); 作用:修改进程的工作目录 比如在/home/wlx 启动了一个可执行程序a.out,进程的工作目录/home/wlx 参数: - path:需要修改的工目录路径
#include <unistd.h> char *getcwd(char *buf, size_t size); 作用:获取当前工作目录 参数: - buf:存储的路径,指向的是一个数组(传出参数) - size:数组的大小 返回值: 返回的是指向的buf的首地址
|
目录操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <fcntl.h> int main(){
char buf[128]; getcwd(buf, sizeof buf); printf("当前的工作目录是:%s\n", buf); int ret = chdir("/root/linux_learn/dir_operation/ddd"); if(ret == -1){ int res = mkdir("ddd", 0664); if(res == -1){ perror("mkdir"); return -1; } chdir("/root/linux_learn/dir_operation/ddd"); } 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, readdir)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <sys/types.h> #include <dirent.h> DIR *opendir(const char *name) 参数: - name:需要打开的目录名称 返回值: - DIR * 类型:理解为目录流 错误返回NULL
#include <dirent.h> struct dirent *readdir(DIR *dirp); 参数: - dirp:通过opendir返回的结果 返回值: - struct dirent:代表读取到的文件信息 读取到了末尾或者失败,返回NULL
// 关闭目录 #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
| #include <sys/types.h> #include <dirent.h> #include <stdio.h> #include <string.h> #include <stdlib.h>
int getFileNum(char *path){
DIR *dir = opendir(path); if(dir == NULL){ perror("opendir"); exit(0); } int res = 0; struct dirent *ptr; 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); res += getFileNum(newpath); } if(ptr->d_type == DT_REG){ res ++; } } closedir(dir); return res; }
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; }
|
文件复制操作(dup, dup2)
| #include <unistd.h> int dup(int oldfd); 作用:复制一个新的文件描述符 fd=3, int fd1 = dup(fd); fd指向的是a.txt, fd1也指向a.txt 从空闲的文件描述符表中找一个最小的,作为新的拷贝的文件描述符
|
dup
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 <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; }
|
| #include <unistd.h> int dup2(int oldfd, int newfd); 作用:重定向文件描述符 old 指向 a.txt, newfd 指向 b.txt 调用函数成功后:newfd 和 b.txt 做close,newfd 指向了 a.txt 等价于: close(newfd), newfd = dup(oldfd); oldfd 必须是一个有效的文件描述符 oldfd 和 newfd 值相同时,相当于无事发生
|
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
| #include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/stat.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
| #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_CREAT 等不可以被修改 O_APPEND, O_NONBLOCK 等可修改 O_APPEND 表示追加数据 O_NONBLOCK 设置成非阻塞
阻塞和非阻塞:描述的是函数调用行为
|
fcntl
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
| #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.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; } int ret = fcntl(fd, F_SETFL, flag | O_APPEND); if(ret == -1){ perror("fcntl"); return -1; } char *str = "nihao"; write(fd, str, strlen(str)); close(fd);
return 0; }
|
多进程开发
并行与并发的区别:
进程状态:
| ps aux / ajx a: 显示终端上所有进程,包括其他用户进程 u: 显示进程详细信息 x: 显示没有控制终端的进程 j: 列出与作业控制相关的信息
# 杀死进程 kill pid kill -9 pid # 强制杀死
|
exec函数族
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 <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ... (char *) NULL ); 参数: - path:需要指定的执行的文件的路径或名称 - arg:可执行文件的参数列表 第一个参数一般没什么作用,通常写的时可执行程序名称 第二个参数开始往后,时程序执行所需要的参数列表 参数最后需要以NULL结束(哨兵) 返回值: 只有当调用失败,才会有返回值,返回-1,并设置errno 如果调用成功,没有返回值。
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[]);
l(list): 参数地址列表,以空指针结尾 v(vector): 存有各参数地址的指针数组的地址 p(path): 按PATH环境变量指定的目录搜索可执行文件 e(environment): 存有环境变量字符地址的指针数组的地址
#include <unistd.h> int execve(const char *filename, char *const argv[], char *const envp[]);
|
exec函数族
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
| #include <unistd.h> #include <stdio.h> extern char **environ; int main(){
pid_t pid = fork(); if(pid > 0){ printf("i am parent process, pid: %d\n", getpid()); sleep(1); }else{ execlp("ps", "ps", "aux", NULL); printf("i am child process, pid: %d\n", getpid()); }
for(int i = 0; i < 3; ++ i){ printf("i = %d, pid = %d\n", i, getpid()); }
return 0; }
|
fork()
和execve()
的原理
进程退出
| #include <stdlib.h> void exit(int status);
#include <unistd.h> void _exit(int status);
printf("hello\nworld"); _exit(0);
|
孤儿进程
- 父进程运行结束,但子进程未结束,就变成孤儿进程。
- 每当出现一个孤儿进程,内核就把它的父进程设置为
init
,而init
进程会循环地wait()
它的已经退出的子进程,作为善后。
- 孤儿进程没什么危害。
僵尸进程
- 每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的 PCB没有办法自己释放掉,需要父进程去释放。
- 进程终止时, 父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(zombie)进程。
- 僵尸进程不能被
kill -9
杀死。
- 这样就会导致一个问题,如果父进程不调用
wait()
或waitpid ()
的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的, 如果大量的产生僵户进程,将因为没有可用的进程号而导致系统不能产生新的进程, 此即为僵尸进程的危害,应当避免。
进程回收(wait 和 waitpid)
- 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
wait()
和waitpid()
函数的功能一样,区别在于, wait()
函数会阻塞,waitpid()
可以设置不阻塞, waitpid()
还可以指定等待哪个子进程结束。
- 注意: 一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
| #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus); 功能:等待任意一个子进程结束,如果有一个子进程结束,则回收资源 参数:int *wstatus 进程退出时的状态信息,传出参数 返回值: - 成功:返回被回收的子进程的pid - 失败:返回 -1(所有子进程都结束,或调用函数失败)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽略的信号才能被唤醒(相当于继续执行) 如果没有子进程,则立刻返回-1 如果子进程都已结束,则立刻返回-1
|
wait
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 <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(){ pid_t pid; for(int i = 0; i < 5; ++ i){ pid = fork(); if(pid == 0){ break; } }
if(pid > 0){ while(1){ printf("parent, pid = %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", WTERMSIG(st)); } printf("child die, pid = %d\n", ret);
sleep(1); } }else if(pid == 0){ printf("child, pid = %d\n", getpid()); sleep(1); exit(1); }
return 0; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #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: 回收在|pid|组内的任意子进程 - options: 设置阻塞或非阻塞 0: 阻塞 WNOHANG: 非阻塞 返回值: > 0: 返回子进程id == 0: options=WNOHANG, 表示还有子进程活着 == -1: 错误,或者没有子进程活着了
|
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
| #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(){ pid_t pid; for(int i = 0; i < 5; ++ i){ pid = fork(); if(pid == 0){ break; } }
if(pid > 0){ while(1){ printf("parent, pid = %d\n", getpid()); sleep(1); int st; 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\n", WTERMSIG(st)); } printf("child die, pid = %d\n", ret); }
} }else if(pid == 0){ while(1){ printf("child, pid = %d\n", getpid()); sleep(1); } exit(0); }
return 0; }
|
进程间通信(IPC)
目的:数据传输、通知事件、资源共享、进程控制
(匿名)管道
- 管道其实是一个在内核内存中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
- 管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
- 一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
- 通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的。
- 在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
- 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用lseek ()来随机的访问数据。
- 匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
为什么可以使用管道进行进程间通信?因为fork()后两个进程共享文件描述符表
管道的数据结构:环形队列(循环队列)622. 设计循环队列 - 力扣(LeetCode)
管道读写特点:
- 如果所有指向管道写端的fd关闭(管道写端引用计数为0),则管道中剩余数据读完后,read会返回0(就像EOF)
- 如果有指向管道写端的fd没有关闭(管道写端引用计数大于0),而持有管道写端的进程也没有往管道写数据,此时读数据,则剩余数据被读完后,再次read会阻塞,直到管道中有数据了才可读并返回
- 如果所有指向管道读端的fd都被关闭(管道读端引用计数为0),这个时候有进程向管道中写数据,那么该进程会收到一个信号SIGPIPE,通常会导致进程异常终止
- 如果有指向管道读端的fd没有关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,此时写数据,则在管道被写满后,再次write会阻塞,直到管道中有空位置才可写并返回
总结:
读管道:
管道中有数据:read返回实际读到的字节数
管道中无数据:
- 写端全关:read返回0(相当于EOF)
- 写端未全关:read阻塞
写管道:
- 管道读端全关:进程异常终止(进程收到SIGPIPE)
- 管道读端未全关:
- 管道已满:write阻塞
- 管道没有满:write将数据写入并返回实际写入字节数
| #include <unistd.h> int pipe(int pipefd[2]); 功能:创建一个匿名管道,用来进程间通信 参数: - int pipefd[2] 这个数组是一个传出参数 pipefd[0] 读端 pipefd[1] 写端 返回值: 成功0,失败-1
管道默认阻塞,如果管道中没有数据,read阻塞,如果满了,write阻塞 注意:匿名管道只能用于具有关系进程之间通信(父子、兄弟 等)
|
pipe 阻塞版
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 <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(){
int pipefd[2]; int ret = pipe(pipefd); if(ret == -1){ perror("pipe"); exit(0); } pid_t pid = fork(); if(pid > 0){ printf("i am parent process, pid = %d\n", getpid()); close(pipefd[1]); char buf[1024] = {0}; while(1){ int len = read(pipefd[0], buf, sizeof buf); printf("parent rcv: %s, pid: %d\n", buf, getpid());
} }else if(pid == 0){ printf("i am child process, pid = %d\n", getpid()); close(pipefd[0]); char buf[1024] = {0}; while(1){ char *str = "hello, i am child"; write(pipefd[1], str, strlen(str));
} }
return 0; }
|
| 设置管道非阻塞 int flags = fcntl(fd[0], F_GETFL); flags |= O_NONBLOCK; fcntl(fd[0], F_SETFL, flags);
|
pipe 非阻塞版
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 <unistd.h> #include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> int main(){
int pipefd[2]; int ret = pipe(pipefd); if(ret == -1){ perror("pipe"); exit(0); } pid_t pid = fork(); if(pid > 0){ printf("i am parent process, pid = %d\n", getpid());
close(pipefd[1]);
int flags = fcntl(pipefd[0], F_GETFL); flags |= O_NONBLOCK; fcntl(pipefd[0], F_SETFL, flags);
char buf[1024] = {0}; while(1){ int len = read(pipefd[0], buf, sizeof buf); printf("len = %d\n", len); printf("parent rcv: %s, pid: %d\n", buf, getpid()); memset(buf, 0, sizeof buf); sleep(1); } }else if(pid == 0){ printf("i am child process, pid = %d\n", getpid());
close(pipefd[0]);
char buf[1024] = {0}; while(1){ char *str = "hello, i am child"; write(pipefd[1], str, strlen(str)); sleep(5); } }
return 0; }
|
| long size = fpathconf(pipefd[0], _PC_PIPE_BUF); printf("pipe size = %d\n", size);
|
实现ps aux 并过滤
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
|
#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; while((len = read(fd[0], buf, sizeof buf))){ printf("%s", buf); memset(buf, 0, sizeof buf); } wait(NULL); }else if(pid == 0){ close(fd[0]); dup2(fd[1], STDOUT_FILENO); execlp("ps", "ps", "aux", NULL); while((ret = execlp("ps", "ps", "aux", NULL)) != -1) {} perror("execlp"); exit(0); }else{ perror("fork"); exit(0); }
return 0; }
|
有名管道
- 有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信,因此,通过FIFO 不相关的进程也能交换数据。
- 一旦打开了FIFO,其他操作同pipe。
FIFO特性:
- FIFO在文件系统中作为一个特殊文件存在,但FIFO中的内容却存放在内存中。
- 当使用FIFO的进程退出后, FIFO文件将继续保存在文件系统中以便以后使用。
| 创建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 & ~umask) 返回值: 成功0,失败-1
|
mkfifo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <stdio.h> #include <unistd.h> int main(){
int ret = access("fifo1", F_OK); if(ret == -1){ printf("管道不存在,创建管道\n");
ret = mkfifo("fifo1", 0664); if(ret == -1){ perror("mkfifo"); exit(0); } }
return 0; };
|
fifo实现进程间通信(read读数据,write写数据)
| 有名管道注意事项: 1. 一个为只读打开一个管道的进程会阻塞,直到另一个进程为只写打卡管道 2. 一个为只写打开一个管道的进程会阻塞,直到另一个进程为只读打卡管道
读管道: 管道中有数据,read返回实际读到的字节数 管道中无数据: 写端全关,read返回0(相当于EOF) 写端未全关,read阻塞等待
写管道: 管道读端全关,进程异常终止(收到一个SIGPIPE信号) 管道读端未全关: 管道已满,write阻塞 管道未满,write写入数据,并返回实际写入字节数
|
write
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 <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main(){
int ret = access("test", F_OK); if(ret == -1){ printf("管道不存在,创建管道\n");
ret = mkfifo("test", 0664); if(ret == -1){ perror("mkfifo"); exit(0); } }
int fd = open("test", O_WRONLY); if(fd == -1){ perror("open"); exit(0); }
for(int i = 0; i < 100; ++ i){ char buf[1024]; sprintf(buf, "hello, %d\n", i); printf("write data : %s\n", buf); write(fd, buf, strlen(buf)); sleep(1); } close(fd);
return 0; };
|
read
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 <stdlib.h> #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> int main(){
int fd = open("test", 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; };
|
使用 父子进程 + FIFO 实现聊天功能
注意,如果仅使用单进程实现A和B,则如果一方连续发送多条消息,会导致阻塞。因此应当采用两个进程,一个进程负责写,另一个负责读。
chatA(父进程写管道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 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
| #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <wait.h> #include <fcntl.h>
void create_fifo(const char *str){ int ret = access(str, F_OK); if(ret == -1){ printf("%s 不存在,创建 %s\n", str, str); ret = mkfifo(str, 0664); if(ret == -1){ perror("mkfifo"); exit(-1); } } }
int main(){
create_fifo("fifo1"); create_fifo("fifo2");
pid_t pid = fork(); int fd1, fd2; if(pid > 0){ fd1 = open("fifo1", O_WRONLY); if(fd1 == -1){ perror("open"); exit(-1); } printf("打开fifo1成功,等待读取...\n"); }else if(pid == 0){ fd2 = open("fifo2", O_RDONLY); if(fd2 == -1){ perror("open"); exit(-1); } printf("打开fifo2成功,等待读取...\n"); }else{ perror("fork"); exit(-1); }
char buf[128]; int ret; while(1){ if(pid > 0){ memset(buf, 0, sizeof buf); fgets(buf, 128, stdin); ret = write(fd1, buf, strlen(buf)); if(ret == -1){ perror("write"); exit(-1); } }else if(pid == 0){ memset(buf, 0, sizeof buf); ret = read(fd2, buf, 127); if(ret <= 0){ perror("read"); break; } printf("buf: %s\n", buf); } }
if(pid > 0) close(fd1); if(pid == 0) close(fd2);
return 0; }
|
chatB(父进程读管道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 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
| #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <wait.h> #include <fcntl.h>
void create_fifo(const char *str){ int ret = access(str, F_OK); if(ret == -1){ printf("%s 不存在,创建 %s\n", str, str); ret = mkfifo(str, 0664); if(ret == -1){ perror("mkfifo"); exit(-1); } } }
int main(){
create_fifo("fifo1"); create_fifo("fifo2");
pid_t pid = fork(); int fd1, fd2; if(pid > 0){ fd1 = open("fifo1", O_RDONLY); if(fd1 == -1){ perror("open"); exit(-1); } printf("打开fifo1成功,等待读取...\n"); }else if(pid == 0){ fd2 = open("fifo2", O_WRONLY); if(fd2 == -1){ perror("open"); exit(-1); } printf("打开fifo2成功,等待读取...\n"); }else{ perror("fork"); exit(-1); }
char buf[128]; int ret; while(1){ if(pid > 0){ memset(buf, 0, sizeof buf); ret = read(fd1, buf, 127); if(ret <= 0){ perror("read"); break; } printf("buf: %s\n", buf); }else if(pid == 0){ memset(buf, 0, sizeof buf); fgets(buf, 128, stdin); ret = write(fd2, buf, strlen(buf)); if(ret == -1){ perror("write"); exit(-1); } } }
if(pid > 0) close(fd1); if(pid == 0) close(fd2);
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
| #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_PRIVE: 不同步,内存映射区数据被改变,对原来的文件不修改,会重新创建新文件(写时复制) - fd: 需要映射的文件的文件描述符 - 通过open得到(一个磁盘文件) - 注意:文件大小不能为0,open指定的权限不能和prot参数冲突 - offset:偏移量,一般不用。必须指定4k的整数倍,0表示不便宜 返回值:返回创建的内存的首地址 失败返回MAP_FAILED,. (void*)-1
int munmap(void *addr, size_t length); 功能:释放内存映射 参数: - addr:要释放的内存的首地址 - length:要释放的内存大小,要和mmap函数中的legnth参数值一样
使用内存映射实现进程间通信: 1. 有关系的进程(父子进程) - 还没有子进程的时候 - 通过唯一的父进程,先创建内存映射区 - 有了内存映射区后,创建子进程 - 父子进程共享创建的内存映射区
2. 没有关系的进程间通信 - 准备一个大小不是0的磁盘文件 - 进程1 通过磁盘文件创建内存映射区 - 得到一个操作这块内存的指针 - 进程2 通过磁盘文件创建内存映射区 - 得到一个操作这块内存的指针 - 使用映射区通信
注意:内存映射区通信,是非阻塞的
|
mmap / munmap
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
| #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <sys/mman.h> #include <stdlib.h> #include <wait.h> int main(){
int fd = open("test.txt", O_RDWR); 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\n", buf); }else if(pid == 0){ strcpy((char *)ptr, "nihao a, son!!"); } munmap(ptr, size);
return 0;
}
|
内存映射经典问题:
- 如果对mmap的返回值(ptr)做++操作(ptr++),munmap能否成功?
可以进行++操作,但是munmap(ptr, len)需要传首地址
- 如果open时O_RDONLY,mmap时prot指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED。open函数权限建议和prot参数权限保持一致(prot时open权限的子集)
偏移量必须是4k的整数倍,否则返回MAP_FAILED
- 第一个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot为PROT_READ | PROT_WRITE,而fd为O_RDONLY / O_WRONLY
- 可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以,但是文件大小不能为0
- 可以对新文件进行扩展:
- mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open(“xxx”);
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd关闭,没有任何影响
void *ptr = mmap(NULL, 100,,,,,);
4K
越界操作,操作的是非法内存 -> segmentation fault
使用文件映射实现文件拷贝功能(通常不会用)
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
|
#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 || 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
|
#include <stdio.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <wait.h> #include <unistd.h> #include <string.h> #include <stdlib.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; }
|
信号
又称为软件中断,时一种异步通信的方式,发往进程的诸多信号通常都源于内核,其事件如下:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入
Ctrl+C
通常会给进程发送一个中断信号。
- 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被0除,或者引用了无法访问的内存区域。
- 系统状态变化,比如alarm定时器到期将引起SIGALRM信号,进程执行的CPU时间超限,或者该进程的某个子进程退出。
- 运行kill 命令或调用kill 函数。
使用信号的两个主要目的是:
- 让进程知道已经发生了一个特定的事情。
- 强迫进程执行它自己代码中的信号处理程序。
信号的特点:
- 简单
- 不能携带大量信息
- 满足某个特定条件才发送优先级比较高
- 查看系统定义的信号列表:
kill -l
- 前31个信号为常规信号,其余为实时信号。
可以通过 man 7 signal
查看文档
五种默认处理动作:Term, Ign, Core, Stop, Cont
kill, raise, abort
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
| #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); 功能:给某个进程或进程组id发送某个信号sig 参数: - pid: > 0:发送给指定进程 = 0:发送给进程组所有进程 = -1: 发送给每一个有权限接受这个信号的进程 < -1:这个pid=某个进程组的ID取反 - sig:需要发送的信号编号或宏值(建议用宏值) kill(getppid(), 9); kill(getpid(), 9);
int raise(int sig); 功能:给当前进程发送信号 参数: - sig:需要发送的信号 返回值: - 成功:0 - 失败:非0 等同于 kill(getpid(), sig);
void abort(void); - 功能:发送SIGABRT信号给当前进程,杀死当前进程 kill(getpid(), SIGABRT);
|
kill
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 <signal.h> #include <unistd.h> #include <stdio.h> int main(){
pid_t pid = fork(); if(pid > 0){ printf("parent\n"); sleep(2); printf("kill child process now\n"); kill(pid, SIGINT); }else if(pid == 0){ int i = 0; for(i=0;i<5;++i){ printf("child\n"); sleep(1); } }
return 0; }
|
alarm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <unistd.h> unsigned int alarm(unsigned int seconds); 功能:设置定时器(闹钟),函数调用开始倒计时,当倒计时为0时,函数会给当前进程发送SIGALARM信号 参数: - seconds:倒计时时长,单位:秒,如果参数为0,则使alarm无效 取消一个定时器,通过 alarm(0) 返回值: - 之前没有定时器:返回0 - 之前有定时器:返回剩余时间
SIGALARM:默认终止当前的进程,每一个进程都有且只有一个唯一的定时器 alarm(10); -> 返回0 过了1秒 alarm(5); -> 返回9
alarm是非阻塞的 实际的时间 = 内核时间 + 用户时间 + 消耗的时间 进行文件IO操作的时候比较浪费时间 定时器:与进程的状态无关(自然定时法),无论进程处于什么状态,alarm都会计时
|
alarm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <unistd.h> #include <stdio.h> int main(){
int seconds = alarm(5); printf("seconds = %d\n", seconds); sleep(2); seconds = alarm(2); printf("seconds = %d\n", seconds);
while(1){ }
return 0; }
|
1s 电脑能数多少个数?
| #include <unistd.h> #include <stdio.h>
int main(){
int seconds = alarm(1); int i = 0; while(1){ printf("%i\n", i++); }
return 0; }
|
setitimer, signal
注意:尽量避免使用signal
,因为它属于ANSI C标准,建议使用sigaction
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
| #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; }; - old_value:记录上一次定时的时间参数,一般不使用,指定NULL 返回值: - 成功:0 - 失败:-1,并设置errno
#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,设置错误号
SIGSTOP SIGSTOP 不能被捕捉,不能被忽略
|
setitimer + 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
| #include <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h>
void myalarm(int num){ printf("捕捉到的信号编号是: %d\n", num); printf("xxxxxxxx\n"); }
int main(){
signal(SIGALRM, myalarm); 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(-1); }
getchar();
return 0; }
|
信号集
- 用户通过键盘Ctrl + C,产生2号信号SIGINT(信号被创建)
- 信号产生但是没被处理(未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中(未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 该标志位为0:不是未决状态
- 该标志位为1:是未决状态
- 这个未决状态的信号需要被处理,处理前需要与阻塞信号集对应标志位比较
- 如果没有阻塞,该信号被处理
- 如果阻塞,该信号处于未决状态,直到阻塞解除该信号才被处理
- 阻塞信号集默认不阻塞任何信号
- 如果想要阻塞某些信号,需要用户调用系统API
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
| #include <signal.h> **以下函数均对自定义信号集操作**
int sigemptyset(sigset_t *set); 功能:清空信号集中的数据,将信号集中的所有标志位置0 参数: - set:传出参数,需要操作的信号集 返回值:成功0,失败-1
int sigfillset(sigset_t *set); 功能:将信号集中的所有标志位置1 其余同上
int sigaddset(sigset_t *set, int signum); 功能:设置信号集中的某一个信号对应的标志位为1,表示阻塞该信号 参数: - set:传出参数,需要操作的信号集 - signum:需要设置阻塞的信号
int sigdelset(sigset_t *set, int signum); 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞该信号 其余同上
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
| #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; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换) 参数: - how:如何对内核阻塞信号集进行处理 SIG_BLOCK:将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变 SIG_UNBLOCK:根据用户设置的数据,对内核中的数据进行接触阻塞 set &= ~mask SIG_SETMASK:覆盖内核中原来的值 - set:已经初始化好的用户自定义的信号集 - oldset:保存设置之前的内核中的阻塞信号集的状态,通常为NULL 返回值: - 成功:0 - 失败:-1 int sigpending(sigset_t *set); 功能:获取内核中的未决信号集 参数: - set:传出参数,保存的是内核中的未决信号集的信息
|
把内核中的常规信号(1-31)的未决状态打印到屏幕
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 <signal.h> #include <stdio.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){ 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("sigismenber"); exit(-1); } } puts(""); sleep(1); if(++num == 10){ sigprocmask(SIG_UNBLOCK, &set, NULL); } } return 0; }
|
sigaction
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 <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); };
|
sigaction捕捉信号
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 <sys/time.h> #include <stdio.h> #include <stdlib.h> #include <signal.h>
void myalarm(int num){ printf("捕捉到的信号编号是: %d\n", num); printf("xxxxxxxx\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(-1); }
while(1) {}
return 0; }
|
SIGCHLD信号
| SIGCHLD 信号产生的3个条件: 1. 子进程结束 2. 子进程暂停了 3. 子进程继续运行 都会给父进程发送该信号,父进程默认忽略该信号。 使用SIGCHLD信号可以解决僵尸进程的问题。
|
SIGCHILD信号捕获
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
| #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> void myFun(int num){ printf("捕捉到的信号:%d\n", num); while(1){ int ret = waitpid(-1, NULL, WNOHANG); if(ret > 0){ printf("child die, pid = %d\n", ret); }else if(ret == 0){ break; }else if(ret == -1){ 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; }
|
共享内存
- 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
- 与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC技术的速度更快。
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
| 共享内存相关的函数 #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 返回值: 成功:>0 返回共性内存引用得ID,之后操作共性内存都是通过这个值 失败:-1,并设置errno
void *shmat(int shmid, const void *shmaddr, int shmflg); 功能:和当前的进程进行关联 参数: - shmid: 共享内存的标识,由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
#include <sys/types.h> #include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id); 功能:根据指定的路径名,和int值,生成一个共享内存的key 参数: - pathname: 指定一个存在的路径 - proj_id: int类型,但是这个系统调用只会使用其中的1个字节 范围: 0~255, 'a'
|
问题1. 操作系统如何直到一块共享内存被多少个进程关联?
- 共享内存维护了一个结构体struct shmid_ds,这个结构体中的成员shm_nattach记录了关联的进程个数
问题2. 可不可以对共享内存进行多次删除?
- 可以,因为shmctl标记删除(key设置为0)共享内存,不是直接删除
- 当和共享内存关联的进程数为0时,真正被删除
问题3. 共享内存与内存映射的区别
- 共享内存可以直接创建,内存映射依赖磁盘文件(匿名映射除外)
- 共享内存效率更高
- 内存映射:所有进程操作的时同一块共享内存
内存映射:每个进程再自己的虚拟地址空间有一个独立的内存
- 数据安全:
- 进程突然退出:
共享内存仍然存在,但内存映射区消失
- 运行进程的电脑宕机
共享内存中的数据无了,内存映射区的数据由于磁盘文件中的数据还在,所以仍存在
- 生命周期
内存映射区:进程退出,内存映射区销毁
共享内存:进程退出,共享内存还在,标记删除(所有关联进程数为0),或者关机。如果一个进程退出,会自动和共享内存取消关联
简单实现共享内存通信:
write.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> #include <stdio.h> int main(){ int shmid = shmget(100, 4096, IPC_CREAT | 0664); printf("shmid : %d\n", shmid); void *ptr = shmat(shmid, NULL, 0); char *str = "hello world"; memcpy(ptr, str, strlen(str)+1); printf("按任意键继续\n"); getchar(); shmdt(ptr); shmctl(shmid, IPC_RMID, NULL); }
|
read.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <sys/ipc.h> #include <sys/shm.h> #include <string.h> #include <stdio.h> int main(){ int shmid = shmget(100, 0, IPC_CREAT); printf("shmid : %d\n", shmid); void *ptr = shmat(shmid, NULL, 0); printf("%s\n", (char *)ptr); printf("按任意键继续\n"); getchar(); shmdt(ptr); shmctl(shmid, IPC_RMID, NULL); }
|
守护进程
即Daemon进程,属于后台服务进程,其具备以下特征:
- 生命周期很长,守护进程会在系统启动的时候被创建并一直运行直至系统被关闭。
- 它在后台运行并且不拥有控制终端。没有控制终端确保了内核永远不会为守护进程自动生成任何控制信号以及终端相关的信号(如SIGINT、SIGQUIT)。
典型案例:Internet服务器inetd,Web服务器httpd等。
创建步骤:
- 执行
fork()
,父进程退出,子进程继续执行
- 子进程调用
setsid()
开启一个新会话(以上两步的原因:确保新会话脱离当前控制终端连接,且进程组id不会冲突,通过子进程调用setsid()会以子进程id作为新进程组和会话的id)
- 清除进程的umask以确保当前守护进程创建文件和目录时拥有所需权限
- 修改进程的当前工作目录,通常会更改为根目录
- 关闭守护进程从其父进程继承而来的所有打开着的的文件描述符
- 在关闭了描述符0、1、2后,守护进程通常会打开/dev/null并使用dup2()使所有这些描述符指向这个设备
- 核心业务逻辑
实现一个守护进程,每隔2s获取系统时间,将这个时间写入到磁盘文件中
code
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
| #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <sys/time.h> #include <signal.h> #include <time.h> #include <stdlib.h> #include <string.h>
void work(int num){ time_t tm = time(NULL); struct tm *loc = localtime(&tm); char * str = asctime(loc); int fd = open("time.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/wlx/");
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 = work; 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; }
|
多线程开发
允许程序并发执行多个任务的一种机制,一个进程可以包含多个线程,同一个程序中的所有线程均会独立执行相同程序,且共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段、堆内存段。
进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位。
线程是轻量级的进程(LWP),在Linux环境下线程的本质仍是进程。
通过ps -Lf pid
获取指定进程的LWP号
为什么要有线程:
- 进程间信息难以共享,必须采用进程间通信方式
- 调用
fork()
创建进程代价较高(即使采用写时复制技术,仍需要复制内存页表、文件描述符表等属性)
- 线程之间能方便快速地共享信息,只需将数据复制到共享(全局或堆)变量中即可
- 创建线程比创建进程快10倍以上,线程间共享虚拟地址空间,无需采用写时复制技术,无需复制页表
线程中共享的资源:
进程ID和父进程ID
进程组ID和会话ID
- 用户ID 和用户组ID
- 文件描述符表
- 信号处置
文件系统的相关信息:文件权限掩码(umask)、当前工作目录
- 虚拟地址空间(除栈、.text)
线程中不共享的资源:
- 线程ID
- 信号掩码
- 线程特有数据
- error变量
- 实时调度策略和优先级
- 栈,本地变量和函数的调用链接信息
查看当前pthred
库的版本:getconf GNU_LIBPTHREAD_VERSION
注意,在编译具有thread的C程序时,需要添加编译命令 -pthread
用来导入thread静态库
pthread库常用函数
线程创建:pthread_create
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 一般情况下,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: 给start_routine使用,传参 返回值: 成功:0 失败:返回错误号(与errno不同,char *strerror(int errnum);
|
pthread_create
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 <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h>
void *callback(void *arg){ printf("child thread...\n"); printf("arg value: %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 * errstr = strerror(ret); printf("error: %s\n", errstr); }
for(int i=0;i<5;++i){ printf("%d\n", i); }
sleep(1);
return 0; }
|
线程退出:pthread_exit
| #include <pthread.h> void pthread_exit(void *retval); 功能:终止当前调用该函数的进程 参数: - retval:指针作为返回值,可以在pthread_join()中获取到
pthread_t pthread_self(void); 功能:获取当前调用线程的id
int pthread_equal(pthread_t t1, pthread_t t2); 功能:比较两个线程ID是否相等 不同操作系统的pthread实现不同,大部分是usigned long,有的是struct
|
pthread_exit
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
| #include <pthread.h> #include <stdio.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; }
|
线程连接:pthread_join
| #include <pthread.h> int pthread_join(pthread_t thread, void **retval); 功能:和一个已经终止的线程进行连接,用于回收子线程资源 特点: 该函数是阻塞函数,调用一次只能回收一个子线程 一般在主线程中使用 参数: - thread: 需要回收的子线程的ID - retval: 接收子线程退出时的返回时 返回值: 成功:0 失败:非零,返回的错误号
|
pthread_join
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 <pthread.h> #include <stdio.h> #include <string.h> #include <unistd.h>
void *callback(void *arg){ printf("child thread id: %ld\n", pthread_self()); sleep(3); static int val = 10; pthread_exit((void *)&val); }
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());
int *thread_retval; ret = pthread_join(tid, (void **)&thread_retval); if(ret != 0){ char *errstr = strerror(ret); printf("error: %s\n", errstr); } printf("exit data: %d\n", *thread_retval); printf("回收子线程资源成功\n");
pthread_exit(NULL); return 0; }
|
线程分离:pthread_detach
| #include <pthread.h> int pthread_detach(pthread_t thread); 功能:分离一个线程,被分离的线程在终止时会自动释放资源返回给系统 1. 不能多次分离,会产生未定义行为 2. 不能区连接已经分离的线程,否则报错 参数: thread:需要分离的线程id 返回值: 成功:0 失败:错误号
|
pthread_detach
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 <unistd.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 *errostr = strerror(ret); printf("error1: %s\n", errostr); }
printf("tid: %ld, main thread id: %d\n", tid, pthread_self());
ret = pthread_detach(tid); if(ret != 0){ char *errostr = strerror(ret); printf("error2: %s\n", errostr); }
ret = pthread_join(tid, NULL); if(ret != 0){ char *errostr = strerror(ret); printf("error3: %s\n", errostr); }
pthread_exit(NULL);
return 0; }
|
线程取消:pthread_cancel
| #include <pthread.h> int pthread_cancel(pthread_t thread); 功能:取消线程(让线程终止) 取消某个线程,可以终止某个线程的运行 但并不是立刻终止,而是到取消点终止
取消点:系统规定好的一些系统调用 可粗略地理解为从用户态到内核态的切换这一点
|
pthread_cancel
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
| #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <string.h>
void *callback(void *arg){ printf("child 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 *errostr = strerror(ret); printf("error1: %s\n", errostr); }
pthread_cancel(tid);
for(int i=0;i<5;++i){ printf("parent: %d\n", i); }
printf("tid: %ld, main thread id: %d\n", tid, pthread_self());
pthread_exit(NULL);
return 0; }
|
线程属性:pthread_attr
| int pthread_attr_init(pthread_attr_t *attr); 功能:初始化线程属性变量
int pthread_attr_destroy(pthread_attr_t *attr); 功能:释放线程属性的资源
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 功能:获取线程分离的状态属性
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 功能:设置线程分离的状态属性
...
|
phtread_attr
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>
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);
size_t size; pthread_attr_getstacksize(&attr, &size); printf("thread stack size: %ld\n",size);
pthread_t tid; int ret = pthread_create(&tid, &attr, callback, NULL); if(ret != 0){ char *errostr = strerror(ret); printf("error1: %s\n", errostr); }
printf("tid: %ld, main thread id: %d\n", tid, pthread_self());
pthread_attr_destroy(&attr); pthread_exit(NULL);
pthread_exit(NULL);
return 0; }
|
---
### 线程同步
- 线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。
- 临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为**原子操作**,也就是同时访问同一共享资源的其他线程不应终端该片段的执行。
- **线程同步**:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。
---
### 互斥锁
- 为避免线程更新共享变量时出现问题,可以使用互斥量(mutual exclusion)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
- 互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
- 一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议:
- 针对共享资源锁定互斥量
- 访问共享资源
- 对互斥量解锁
- 如果多个线程试图执行这一块代码(一个临界区),事实上只有一个线在能够持有该旦斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如下图所示:
![互斥锁保证线程同步](https://blog-1301883815.cos.ap-nanjing.myqcloud.com/img/image-20220916155535865.png)
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 <pthread.h> 互斥量的类型: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 50 51 52 53 54 55 56 57 58 59 60
|
#include <stdio.h> #include <pthread.h> #include <unistd.h>
int tickets = 100000;
pthread_mutex_t mutex;
void *sellticket(void *arg){
while(tickets){ pthread_mutex_lock(&mutex); if(tickets){ printf("%ld 正在卖第 %d 张门票\n", pthread_self(), tickets); tickets--; } pthread_mutex_unlock(&mutex); }
return NULL; }
int main(){
pthread_mutex_init(&mutex, NULL);
pthread_t tid[3]; for(int i=0;i<3;++i){ pthread_create(&tid[i], NULL, sellticket, NULL); }
for(int i=0;i<3;++i){ pthread_detach(tid[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
| #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); 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(&tid1, NULL, workB, NULL);
pthread_join(tid1, NULL); pthread_join(tid2, NULL);
pthread_mutex_destroy(&mutex1); pthread_mutex_destroy(&mutex2);
return 0; }
|
读写锁
- 实际上,对共享资源只进行读操作并不会造成线程同步的问题,因此诞生了读写锁。
- 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
- 读写锁的特点:
- 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
- 如果有其它线程写数据,则其它线程都不允许读、写操作。
- 写是独占的,写的优先级高。
| #include <pthread.h> 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个线程不定时读这个全局变量
|
rwlock
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 <pthread.h> #include <stdio.h> #include <unistd.h>
int num = 1;
pthread_rwlock_t rwlock;
void *writeNumber(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 *readNumber(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 wtids[3], rtids[5]; for(int i=0;i<3;++i){ pthread_create(&wtids[i], NULL, writeNumber, NULL); } for(int i=0;i<5;++i){ pthread_create(&rtids[i], NULL, readNumber, NULL); }
for(int i=0;i<3;++i){ pthread_detach(wtids[i]); } for(int i=0;i<5;++i){ pthread_detach(rtids[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
| #include <pthread.h>
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); - 唤醒所有的等待的线程
|
cond + 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 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
|
#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 * customer(void *arg){
while(1){ pthread_mutex_lock(&mutex); if(head != NULL){ struct Node *temp = head; head = head->next; printf("del node, num: %d, tid: %ld\n", temp->num, pthread_self()); free(temp); pthread_mutex_unlock(&mutex); usleep(100); }else{ pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); } }
return 0; }
int main(){
pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL);
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_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
| #include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value); 功能:初始化信号量 参数: - sem:信号量变量的地址 - pshared: - 0:用在线程间 - 1:用在进程间 - 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);
|
semaphore + 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 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
|
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.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); usleep(100); }
return NULL; }
void * customer(void *arg){
while(1){ sem_wait(&csem); pthread_mutex_lock(&mutex); struct Node *temp = head; head = head->next; printf("del node, num: %d, tid: %ld\n", temp->num, pthread_self()); free(temp); pthread_mutex_unlock(&mutex); sem_post(&psem); usleep(100); }
return 0; }
int main(){
sem_init(&psem, 0, 8); sem_init(&csem, 0, 0);
pthread_mutex_init(&mutex, NULL);
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; }
|