Linux与操作系统

一些OS和Linux的前置姿势


Linux & OS

静态库和动态库

  • 静态库:链接时将静态库内容复制到程序中,与程序一起打包成可执行文件

    • 优点:加载速度快;发布时无需提供静态库,移植方便
    • 缺点:消耗内存;更新部署发布麻烦
  • 动态库(共享库):链接时将动态库相关信息与程序链接,真正使用函数api是直到运行时才动态载入

    • 优点:进程间资源共享;更新部署发布简单;可以控制何时加载动态库
    • 缺点:加载速度较静态库慢;发布时需要提供依赖库

静态库

steps for build a static link library:

  1. gcc -c a.c b.c xxx.c ...

  2. ar rcs libxxx.a a.o b.o ... -I ./include_dir

  3. complie the main code (don’t forget to add -I and -L -L ./lib_dir -lxxx)

动态库

steps for build a dynamic link library:

  1. gcc -c -fpic a.c b.c xxx.c ...(-fpic代表生成与位置无关的代码)

  2. gcc -shared a.o b.o ... -o libxxx.so

  3. complie the main code (don’t forget to add -I and -L)

  4. 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

基本操作

makefile操作

变量

通配符匹配

函数

wildcard函数(用于返回满足匹配的文件名列表)

patsubst(用于replacement)

.PHONY: 后跟的名称为伪文件(即:不是一个文件),用于防止和自己定义的文件名冲突而使规则无效化(一直显示最新)

Makefile的特点:

  • 执行make前,先检查依赖是否存在,若不存在,则向下检查是否有生成该规则的依赖并执行

  • 检测更新:如果依赖文件比目标文件的时间晚,则会重新生成目标

举例:一个文件树如下,其中a.c, b.c定义了头文件head.h声明的函数,而main.c使用了a.c, b.c定义的函数。此处我想把a.c, b.c打包成静态库,并编译main.c

work tree

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 # 程序停在main第一行
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 # 设置debug时是否运行其他进程
info inferiors # 查看调试的进程
inferior id # 切换当前调试进程
detach inferiors id # 使进程脱离GDB调试

虚拟地址空间与文件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> // close
#include <stdio.h> // perror
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(){

// 通过open打开english.txt
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

struct stat结构体信息

  • 获取文件指定的权限:将st_mode和对应宏进行按位与

  • 获取文件类型:将st_mode和对应掩码(S_IFMT)进行按位与,结果与宏值对应判断

mode_t类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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
// 模拟实现 ls -l 指令
// -rw-r--r-- 1 root root 12 Aug 30 11:02 a.txt

#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;
}

// 通过stat函数获取用户传入文件的信息
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';
// printf("%s\n", perms);

// 硬链接数
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;
}

最终效果:

模拟实现ls -l a.txt最终效果


文件属性操作函数(access, chmod, chown, truncate)

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>
int access(const char *pathname, int mode);
作用:判断某文件是否有某种权限,或是否存在
参数:
pathname:文件名
mode:
R_OK: 是否有读权限
W_OK: 判断是否有写权限
X_OK: 判断是否有执行权限
F_OK: 判断是否存在
返回值:
成功返回0, 失败返回-1
access
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");
return -1;
}

printf("File exists!!!\n");

return 0;
}

1
2
3
4
5
6
7
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
作用:修改文件权限
参数:
- pathname:文件路径
- mode:需要修改的权限,8进制数
返回值:成功0,失败-1
chmod
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", 0644);
if(ret == -1){
perror("chmod");
return -1;
}
return 0;
}

chown
1
2
3
4
5
6
/*

#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
作用:改变文件所有者
*/

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
truncate
1
2
3
4
5
6
7
8
9
10
11
12
#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)

目录操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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)

struct dirent结构体

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){

// 1. 打开目录
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)

1
2
3
4
5
6
#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;
}

1
2
3
4
5
6
7
8
#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;
}

// 通过fd1去写数据,实际操作的是1.txt
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(){

// 1. 复制文件描述符
// int fd = open("1.txt", O_RDONLY);
// int ret = fcntl(fd, F_DUPFD);

// 2. 修改或获取文件状态flag
int fd = open("1.txt", O_RDWR);
if(fd == -1){
perror("open");
return -1;
}

// 获取文件描述符的状态flag
int flag = fcntl(fd, F_GETFL);
if(flag == -1){
perror("fcntl");
return -1;
}
// 修改文件描述符状态flag,给flag加入O_APPEND标记
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;
}

多进程开发

并行与并发的区别:

并行(左)与并发(右)的区别

进程状态:

进程状态示意图

1
2
3
4
5
6
7
8
9
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(){

// 创建一个子进程,在子进程中执行exec函数族中的函数
pid_t pid = fork();
if(pid > 0){
// parent
printf("i am parent process, pid: %d\n", getpid());
sleep(1);
}else{
// child
// execl("hello", "hello", NULL);
// execl("/bin/ps", "ps", "aux", NULL);
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()的原理

  • fork()函数原理:

    • 被当前进程调用时,内核为新进程创建数据结构,并分配一个唯一的pid
    • 创建虚拟内存:创建mm_struct,区域结构和页表的原样副本
    • 将两个进程的页表都标记为只读
    • 将两个进程的每个区域结构标记为私有的写时复制(只要有一个进程试图写私有区域的某个页面,则触发保护故障,在物理内存中页面的新副本,更新页表条目指向新副本,恢复可写权限)
  • execve()函数原理:(例如,在当前进程执行execve("a.out", NULL, NULL)

    • 删除已存在的用户区域
    • 映射私有区域:为新程序的代码、数据、bss和栈区创建新的区域结构,这些新区域都是私有的写时复制的(其中代码和数据被映射到.text, .data
    • 映射共享区域:如果a.out与共享对象链接(例如 动态链接库libxxx.so),则映射到虚拟地址空间中,这些区域是共享的
    • 设置程序计数器(PC):使之指向代码区域入口

进程退出

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
void exit(int status);
// 调用退出处理函数
// 刷新I/O缓冲,关闭文件描述符
// 调用_exit()系统调用

#include <unistd.h>
void _exit(int status);
// 终止进程


printf("hello\nworld");
_exit(0); // 未刷新缓冲区,world无法输出

孤儿进程

  • 父进程运行结束,但子进程未结束,就变成孤儿进程。
  • 每当出现一个孤儿进程,内核就把它的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程,作为善后。
  • 孤儿进程没什么危害

僵尸进程

  • 每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的 PCB没有办法自己释放掉,需要父进程去释放。
  • 进程终止时, 父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(zombie)进程。
  • 僵尸进程不能被 kill -9 杀死
  • 这样就会导致一个问题,如果父进程不调用wait()waitpid ()的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的, 如果大量的产生僵户进程,将因为没有可用的进程号而导致系统不能产生新的进程, 此即为僵尸进程的危害,应当避免

进程回收(wait 和 waitpid)

  • 父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
  • wait()waitpid()函数的功能一样,区别在于, wait()函数会阻塞waitpid()可以设置不阻塞, waitpid()还可以指定等待哪个子进程结束
  • 注意: 一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
#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(){

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
for(int i = 0; i < 5; ++ i){
pid = fork();
if(pid == 0){
break;
}
}

if(pid > 0){
// parent
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){
// child
// while(1){
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(){

// 有一个父进程,创建5个子进程(兄弟)
pid_t pid;
for(int i = 0; i < 5; ++ i){
pid = fork();
if(pid == 0){
break;
}
}

if(pid > 0){
// parent
while(1){
printf("parent, pid = %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\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
}

}
}else if(pid == 0){
// child
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将数据写入并返回实际写入字节数

1
2
3
4
5
6
7
8
9
10
11
12
#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(){

// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1){
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0){
// parent
// 从管道读取端读取数据,同时发送数据
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());

// char *str = "hello, i am parent\n";
// write(pipefd[1], str, strlen(str));
// sleep(1);
}
}else if(pid == 0){
// child
// 向管道中写入数据,同时接受数据
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(1);
// 注意!这里可能会出现自己读自己写的数据的情况!

// int len = read(pipefd[0], buf, sizeof buf);
// printf("child rcv: %s, pid: %d\n", buf, getpid());
// bzero(buf, 1024);
}
}

return 0;
}

1
2
3
4
设置管道非阻塞
int flags = fcntl(fd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(fd[0], F_SETFL, flags); // 设置flag
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(){

// 在fork之前创建管道
int pipefd[2];
int ret = pipe(pipefd);
if(ret == -1){
perror("pipe");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if(pid > 0){
// parent
printf("i am parent process, pid = %d\n", getpid());

// 关闭写端
close(pipefd[1]);

int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flag
flags |= O_NONBLOCK; // 修改flag的值
fcntl(pipefd[0], F_SETFL, flags); // 设置flag

// 从管道读取端读取数据
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){
// child
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;
}

1
2
3
// 获取管道大小
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
/*

实现 ps aud | grep xxx 父子进程间通信
子进程:ps aux, 子进程结束后,将数据发送给父进程
父进程:获取到数据,过滤

pipe()
execlp()
子进程将标准输出stdout_fileno重定向到管道写端:dup2()

*/
#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){
// parent
// 关闭写端
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){
// child
// 关闭读端
close(fd[0]);
// 文件描述符重定向: stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 执行 ps aux
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文件将继续保存在文件系统中以便以后使用。

1
2
3
4
5
6
7
8
9
10
11
12
创建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
3
4
5
6
7
8
9
10
11
12
13
14
15
有名管道注意事项:
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(){

// 1. 判断文件是否存在
int ret = access("test", F_OK);
if(ret == -1){
printf("管道不存在,创建管道\n");

// 2. 创建管道文件
ret = mkfifo("test", 0664);
if(ret == -1){
perror("mkfifo");
exit(0);
}
}

// 3. 以只写方式打开管道
int fd = open("test", O_WRONLY);
if(fd == -1){
perror("open");
exit(0);
}

// 4. 写数据
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(){

// 1. 打开管道文件
int fd = open("test", O_RDONLY);
if(fd == -1){
perror("open");
exit(0);
}

// 2. 读数据
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(){

// 创建fifo管道
create_fifo("fifo1");
create_fifo("fifo2");

// fork出一个子进程
pid_t pid = fork();
int fd1, fd2;
if(pid > 0){
// 父进程以只写方式打开1
fd1 = open("fifo1", O_WRONLY);
if(fd1 == -1){
perror("open");
exit(-1);
}
printf("打开fifo1成功,等待读取...\n");
}else if(pid == 0){
// 子进程以只读方式打开2
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){
// 父进程循环向fifo1写入数据
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){
// 子进程循环读fifo2
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(){

// 创建fifo管道
create_fifo("fifo1");
create_fifo("fifo2");

// fork出一个子进程
pid_t pid = fork();
int fd1, fd2;
if(pid > 0){
// 父进程以只读方式打开1
fd1 = open("fifo1", O_RDONLY);
if(fd1 == -1){
perror("open");
exit(-1);
}
printf("打开fifo1成功,等待读取...\n");
}else if(pid == 0){
// 子进程以只写方式打开2
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){
// 父进程循环读fifo1
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){
// 子进程循环写fifo2
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(){

// 1. 打开一个文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);

// 2. 创建内存映射区
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap");
exit(0);
}

// 3. 创建子进程
pid_t pid = fork();
if(pid > 0){
wait(NULL);
// parent
char buf[64];
strcpy(buf, (char *)ptr);
printf("read data: %s\n", buf);
}else if(pid == 0){
// child
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权限的子集)

  • 如果文件偏移量为1000会怎样?

偏移量必须是4k的整数倍,否则返回MAP_FAILED

  • mmap什么情况下会调用失败?
  • 第一个参数:length = 0
  • 第三个参数:prot
    • 只指定了写权限
    • prot为PROT_READ | PROT_WRITE,而fd为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
越界操作,操作的是非法内存 -> 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
// 使用文件映射实现文件拷贝功能(通常不会用)

/*

思路:
1. 对原始文件进行内存映射
2. 创建新文件(扩展)
3. 把新文件数据映射到内存中
4. 通过内存映射将第一个文件的内存数据拷贝到新文件中
5. 释放数据

*/
#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(){

// 1. 对原始文件进行内存映射
int fd = open("english.txt", O_RDWR);
if(fd == -1){
perror("open");
exit(0);
}

// 获取原始文件的大小
int len = lseek(fd, 0, SEEK_END);

// 2. 创建新文件(扩展)
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
if(fd1 == -1){
perror("open");
exit(0);
}

// 对新文件进行扩展
truncate("cpy.txt", len);
write(fd1, " ", 1);

// 3. 分别做内存映射
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);
}

// 4. 通过内存映射将第一个文件的内存数据拷贝到新文件中
memcpy(ptr1, ptr, len);

// 5. 释放资源
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(){

// 1. 创建匿名内存映射区
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);
}
// 2. 父子进程间通信
pid_t pid = fork();
if(pid > 0){
// parent
strcpy((char *)ptr, "hello world");
wait(NULL);
}else if(pid == 0){
// child
sleep(1);
printf("%s\n", (char *)ptr);
}

// 3. 释放内存映射区
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

常用信号1

常用信号2

常用信号3


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); // 0

sleep(2);
seconds = alarm(2); // 不阻塞
printf("seconds = %d\n", seconds); // 3

while(1){

}

return 0;
}

1s 电脑能数多少个数?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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>
// 过3s后,每隔2s定时一次

void myalarm(int num){ // 自定义回调函数
printf("捕捉到的信号编号是: %d\n", num);
printf("xxxxxxxx\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后第一次发送信号,然后每隔2s发送一次
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;
}

信号集

阻塞信号集和未决信号集

  1. 用户通过键盘Ctrl + C,产生2号信号SIGINT(信号被创建)
  2. 信号产生但是没被处理(未决)
    • 在内核中将所有的没有被处理的信号存储在一个集合中(未决信号集)
    • SIGINT信号状态被存储在第二个标志位上
      • 该标志位为0:不是未决状态
      • 该标志位为1:是未决状态
  3. 这个未决状态的信号需要被处理,处理前需要与阻塞信号集对应标志位比较
    • 如果没有阻塞,该信号被处理
    • 如果阻塞,该信号处于未决状态,直到阻塞解除该信号才被处理
  • 阻塞信号集默认不阻塞任何信号
  • 如果想要阻塞某些信号,需要用户调用系统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);

// 判断 SIGINT 是否在信号集 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);

// 判断 SIGINT 是否在信号集中
ret = sigismember(&set, SIGINT);
if(ret == 0){
printf("SIGINT 不阻塞\n");
}else if(ret == 1){
printf("SIGINT 阻塞\n");
}
// 判断 SIGQUIT 是否在信号集中
ret = sigismember(&set, SIGQUIT);
if(ret == 0){
printf("SIGQUIT 不阻塞\n");
}else if(ret == 1){
printf("SIGQUIT 阻塞\n");
}

// 从信号集中删除一个信号
sigdelset(&set, SIGQUIT);

// 判断 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
// 编写一个程序,把内核中的常规信号(1-31)的未决状态打印到屏幕
// 设置某些信号是阻塞的,通过键盘产生这些信号
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
// 设置2、3信号阻塞
sigset_t set;
sigemptyset(&set);
// 将2、3信号添加到信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内存中的阻塞信号集
sigprocmask(SIG_BLOCK, &set, NULL);

int num = 0;

while(1){
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&pendingset);
sigpending(&pendingset);
// 遍历前32位
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;
// 使用哪一个信号处理对捕捉到的信号进行处理
// 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
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>
// 过3s后,每隔2s定时一次
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;
// 设置延迟时间,3s后第一次发送信号,然后每隔2s发送一次
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信号
1
2
3
4
5
6
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);
// 回收子进程PCB资源
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(){

// 提前设置好阻塞信号集,阻塞SITCHLD,因为有可能子进程很快结束,父进程还没捕捉到
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){
// parent
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){
// child
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. 共享内存与内存映射的区别

  1. 共享内存可以直接创建,内存映射依赖磁盘文件(匿名映射除外)
  2. 共享内存效率更高
  3. 内存映射:所有进程操作的时同一块共享内存
    内存映射:每个进程再自己的虚拟地址空间有一个独立的内存
  4. 数据安全:
    • 进程突然退出:
      共享内存仍然存在,但内存映射区消失
    • 运行进程的电脑宕机
      共享内存中的数据无了,内存映射区的数据由于磁盘文件中的数据还在,所以仍存在
  5. 生命周期
    内存映射区:进程退出,内存映射区销毁
    共享内存:进程退出,共享内存还在,标记删除(所有关联进程数为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(){
// 1. 创建一个共享内存
int shmid = shmget(100, 4096, IPC_CREAT | 0664);
printf("shmid : %d\n", shmid);
// 2. 和当前进程进行关联
void *ptr = shmat(shmid, NULL, 0);
// 3. 写数据
char *str = "hello world";
memcpy(ptr, str, strlen(str)+1);
printf("按任意键继续\n");
getchar();
// 4. 解除关联
shmdt(ptr);
// 5. 删除共享内存
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(){
// 1. 获取一个共享内存
int shmid = shmget(100, 0, IPC_CREAT);
printf("shmid : %d\n", shmid);
// 2. 和当前进程进行关联
void *ptr = shmat(shmid, NULL, 0);
// 3. 读数据
printf("%s\n", (char *)ptr);
printf("按任意键继续\n");
getchar();
// 4. 解除关联
shmdt(ptr);
// 5. 删除共享内存
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(){

// 1. 创建子进程,退出父进程
pid_t pid = fork();

if(pid > 0){
exit(0);
}
// 2. 将子进程重新创建一个会话
setsid();

// 3. 设置掩码
umask(022);

// 4. 更改工作目录
chdir("/home/wlx/");

// 5. 关闭、重定向文件描述符
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);

// 6. 业务逻辑

// 捕捉定时信号
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; // exit(0)
}

线程退出:pthread_exit

1
2
3
4
5
6
7
8
9
10
11
12
#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; // pthread_exit(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

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
失败:非零,返回的错误号
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); // return (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());

// 主线程调用pthread_join回收子线程资源
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

1
2
3
4
5
6
7
8
9
10
#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);
}

// 输出主线程和子线程的ID
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);
}

// 设置分离后,对分离的子线程进行连接 pthread_join()
ret = pthread_join(tid, NULL);
if(ret != 0){
char *errostr = strerror(ret);
printf("error3: %s\n", errostr); // 会报错
}

pthread_exit(NULL);

return 0;
}

线程取消:pthread_cancel

1
2
3
4
5
6
7
8
#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);
}

// 输出主线程和子线程的ID
printf("tid: %ld, main thread id: %d\n", tid, pthread_self());

// 退出主线程
pthread_exit(NULL);

return 0;
}

线程属性:pthread_attr

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

// 输出主线程和子线程的ID
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
/*
使用多线程实现买票案例
一共100张票,3个窗口并发卖票
*/
#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);

// 创建3个子线程
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_join(tid[i], 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;
}

读写锁

  • 实际上,对共享资源只进行读操作并不会造成线程同步的问题,因此诞生了读写锁
  • 在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
  • 读写锁的特点:
    • 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
    • 如果有其它线程写数据,则其它线程都不允许读、写操作。
    • 写是独占的,写的优先级高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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);

// 创建3个写线程、5个读线程
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);

// 创建5个生产者线程,5个消费者线程
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
/*

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);
}
*/

#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);

// 创建5个生产者线程,5个消费者线程
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;
}


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!