0%

Linux系统编程

Shell脚本

TODO

系统调用

TODO

进程

TODO

信号

进程间通信概述

进程间通信(IPC):

​ 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的, 没有 关联,不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)。

​ 进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。

进程间通信功能:

数据传输:一个进程需要将它的数据发送给另一个进程。

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。

进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程

​ 的所有操作,并能够及时知道它的状态改变。

Linux操作系统支持的主要进程间通信的通信机制

1614856745682

进程间通信的实质:

系统只要创建一个进程,就会给当前进程分配4G的虚拟内存(32位操作系统)。03G为用户空间,34G的内核空间,

用户空间是进程私有的,只能自己访问和使用。堆栈、数据区、代码区都是用户空间。

内核空间是所有进程公有。绝大多数进程间通信方式,是对内核空间操作

特殊的进程间通信方式:

  • socket通信可以实现不同主机的进程间通信。

  • 信号通信是唯一的一种异步通信机制。

  • 共享内存是所有IPC中效率最高的,直接对物理内存操作。

信号通信

  • 信号是软件中断,在软件层次上对中断机制的一种模拟。
  • 信号是一种异步的通信机制,进程不必等待信号的到达、进程也不知道信号什么时候到达
  • 信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以通过他来通知用户空间进程发生了哪些系统事件
  • 每个信号的名字都以SIG开头
  • 使用kill -l可以列出所有信号

1614858743604

  • 信号都是定义好的。不用自己定义

产生信号的方式

  1. 用户按某些终端键时,将产生信号

    例如:“Ctrl+C”

  2. 硬件异常

    例如:无效的内存访问(除数为0)

  3. 软件异常

  4. 调用kill函数

  5. 运行kill命令

信号的默认处理方式

  1. 终止进程:当信号产生后,当前进程立即结束
  2. 缺省处理:当前进程不做任何处理
  3. 停止进程:当前进程停止
  4. 让停止进程恢复运行:当信号产生后,停止的进程恢复执行(后台进程)

PS:每一个信号只有一个默认的处理方式

进程收到信号的处理方式

  1. 执行系统默认动作
  2. 忽略此进程
  3. 执行自定义的信号处理函数

PS:SIGKILLSIGSTOP这两个信号只能以默认的处理方式执行,不能忽略、自定义

常见信号

TODO

信号基本操作

kill函数

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
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int signum);
/*
功能:
给指定进程发送信号
参数:
pid: 四种情况
signum: 信号编号

pid四种情况:
pid>0: 将信号传送给进程 ID 为 pid 的进程。
pid=0: 将信号传送给当前进程所在进程组中的所有进程。
pid=-1: 将信号传送给系统内所有的进程。
pid<-1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。
*/
//示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
else if(pid > 0) //
{
while(1)
{
printf("this is father proc\n");
sleep(1);
}
}
else
{
printf("this is son proc\n");
//子进程在3s后,让父进程退出
sleep(3);

///使用kill给父进程发送信号
kill(getppid(), SIGINT);
}

return 0;
}

//执行结果
this is father proc
this is son proc
this is father proc
this is father proc

alarm函数

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 <unistd.h>

unsigned int alarm(unsigned int seconds);
/*
功能:
定时器、闹钟,当设定的时间到达时,会产生SIGALRM信号
参数:
seconds:设定的秒数

pid四种情况:
如果alarm函数之前没有alarm设置,则返回0
如果有,则返回上一个alarm剩余的时间
*/
//示例
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
//当执行完alarm后,代码会接着执行,当设定时间到后,会产生SIGALRM信号

//如果alarm之前没有设置其他闹钟,则返回0,如果之前设置了,则返回之前剩余的秒数
//如果一个程序中出现多个alarm闹钟,第一个如果没有到达指定的时间爱你就遇到第二个闹钟
// 则第一个闹钟的时间清除,按照第二个alarm闹钟时间继续向下运行
unsigned int sec;

sec = alarm(5);
printf("sec = %d\n", sec);

sleep(3);

sec = alarm(6);
printf("sec = %d\n", sec);

while (1)
{
printf("hello world\n");
sleep(1);
}

return 0;
}


//执行结果
sec = 0
sec = 2
hello world
hello world
hello world
hello world
hello world
hello world
Alarm clock

raise函数

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

int raise(int sig);

功能:
给调用进程本身发送信号
参数:
sig: 指定的信号
返回值:
成功 0
失败 非0

//案例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{

int num = 0;

while (1)
{
printf("hello world\n");
sleep(1);

num++;
// 当循环执行5s后,进程退出
if(num == 5)
{
// 使用raise给当前进程本身发送信号
raise(SIGINT);
}
}

return 0; //
}

//执行结果
hello world
hello world
hello world
hello world
hello world

abort函数

即使SIGABRT信号加入阻塞集,一旦进程调用了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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdlib.h>

void abort(void);

功能:向进程发送一个SIGABRT信号,默认情况下进程会退出
参数:无
返回值:无

//案例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{

int num = 0;

while (1)
{
printf("hello world\n");
sleep(1);

num++;
// 当循环执行5s后,进程退出
if (num == 5)
{
// 使用raise给当前进程本身发送信号
abort();
}
}

return 0; //
}

//执行结果
hello world
hello world
hello world
hello world
hello world
Aborted

pause函数

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

int pause(void);
功能:
将调用进程挂起直至捕捉到信号为止。这个函数通常用于判断信号是否已到。
返回值:
直到捕捉到信号,pause信号才返回-1,且errno被设置成EINTR

//案例
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char **argv)
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fork");
exit(1);
}
else if (pid > 0) //
{
printf("this is father proc\n");
// 使用pause函数阻塞等待捕捉信号
pause();
}
else
{
printf("this is son proc\n");
//子进程在3s后,让父进程退出
sleep(3);

///使用kill给父进程发送信号
kill(getppid(), SIGINT);
}

return 0;
}

//执行结果
this is father proc
this is son proc

signal函数

程序中可用signal()改变信号的处理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
/*
功能:注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。

参数:
signum:信号编号
handler:处理方式
SIG_IGN 当信号产生时,以缺省(忽略)的方式处理
SIG_DFL 当信号产生时,以当前信号默认的方式处理
自定义信号处理函数:信号处理函数名
返回值:
成功:返回函数指针,该地址为此信号上一次注册的信号处理函数的地址
失败:SIG_ERR
*/
signal函数的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int sig);

int main(int argc, char **argv)
{
//以默认的方式处理信号
#if 0
if (signal(SIGINT, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGTSTP, SIG_DFL) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif

//以忽略的方式处理信号
#if 0
if (signal(SIGINT, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGQUIT, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGTSTP, SIG_IGN) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
//kill\stop信号只能以默认的方式处理,不能忽略或者捕捉
// if (signal(SIGKILL, SIG_IGN) == SIG_ERR)
// {
// perror("fail to signal");
// exit(1);
// }
#endif

//以用户自定义的方式处理信号
#if 1
if (signal(SIGINT, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGQUIT, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
if (signal(SIGTSTP, handler) == SIG_ERR)
{
perror("fail to signal");
exit(1);
}
#endif

while (1)
{
printf("hello world\n");
sleep(1);
}

return 0;
}

void handler(int sig)
{
if (sig == SIGINT)
{
printf("SIGINT正在处理\n");
}
if (sig == SIGQUIT)
{
printf("SIGQUIT正在处理\n");
}
if (sig == SIGTSTP)
{
printf("SIGTSTP正在处理\n");
}
}
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
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler(int sig);
void *ret_handler;

int main(int argc, char **argv)
{
if((ret_handler = signal(SIGINT,handler)) == SIG_ERR)
{
perror("fail to signal\n");
exit(1);
}

while (1)
{
printf("hello world\n");
sleep(1);
}

return 0;
}

void handler(int sig)
{
printf("*****************\n");
printf("test\n");
printf("*****************\n");

if (signal(SIGINT, ret_handler) == SIG_ERR)
{
perror("fail to signal\n");
exit(1);
}
}

可重入函数

可重入函数是指函数可以由多个任务并发使用,而不必担心数据错误

可重入函数就是可以被中断的函数,当前函数可以在任何时刻中断它,并执行另一块代码。当执行完毕后,回到原本的代码还可以继续正常运行

编写可重入函数:

  • 不使用(返回)静态的数据、全局变量(除非用信号量互斥)
  • 不调用动态内存分配、释放函数
  • 不调用任何不可重入的函数(如标准I/O函数)

即使信号处理函数使用的都是可重入函数(常见的可重入函数),也要注意进入处理函数时,首先要保存errno的值,结束时,在恢复原值。因为在信号处理过程中,errno值随时可能被改变

信号集

信号集概述

一个用户进程常常需要对多个信号做出处理。为了方便对多个信号进行处理,在Linux系统中引入了信号集。信号集是用来表示多个信号的数据结构。

信号集数据类型

sigset_t

管道、命名管道

无名管道概述

管道又叫无名管道

无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符

任何个进程创建的时候,系统都会给他分配4G的虚拟内存,分为3G用户空间和1G内核空间,内核空间是所有进程公有的,无名管道就是创建在内核空间的,多个进程知道同一个无名管道的空间,就可以利用他进行通信

image-20210314212606622

无名管道虽然是在内核空间创建的,但是会给当前用户进程两个文件描述符,一个负责读操作,一个负责执行写操作。

管道特点:

  1. 半双工,数据在同一时刻只能在一个方向上流动。
  2. 数据只能从管道的一端写入,另一端读出。
  3. 写入管道中的数据符合先进先出的规则。
  4. 管道传送的数据是无格式的,这要求管道的双方约定好数据格式
  5. 管道不是普通文件,不属于某一个文件系统,只存在内存中
-------------THE END-------------

欢迎关注我的其它发布渠道