0%

进程以及进程间的通信

本文包含进程的介绍和使用,以及进程间的通信方式。

进程

进程的概念

进程是程序的一次执行过程,进程是一个正在执行的任务,进程是分配资源的最小单位。每一个进程都会分配自己的0-3G的内存空间,0-3G的内存空间有多份,而3-4G内核空间只有一份。在这0-3G内存空间中堆区、栈区、静态区(缓冲区,文件描述符)。

进程其实是内核创建的,每个进程在内核空间都对应的是一个 task_struct(PCB) 的结构体。正在运行的进程会被放到一个运行队列中,随着时间片轮询依次来执行进程。一个进程的崩溃不会影响另外一个进程的执行,进程的安全性高

进程和程序的区别

程序:程序是静态的,没有生命周期的概念,它是有序的指令的集合,在硬盘上存储着

进程:进程是程序的一次执行过程,它是由生命周期的,随着程序的执行而运行,随着程序的终止而结束,在内存上存储着。可以分配自己的0-3G的内存空间

进程的组成

  • 进程控制块PCB task_struct
    进程的标识符PID,进程运行的状态,进程所属的用户uid,内存空间…
  • 文本段
    存放可执行程序本身
  • 数据段
    存放程序运行时候产生的数据,比如int a;a就在数据段存放

进程的种类

  • 交互进程 :交互进程是由shell维护的,通过shell和用户进行交互,例如文本编辑器就是一个交互进程。

  • 批处理进程:批处理进程的优先级比较低,通常情况下批处理进程都会被放到队列中执行,例如gcc编译程序的过程就是批处理进程。

  • 守护进程 :守护进程是一个后台运行的进程,随着系统的启动而启动,随着系统的终止而终止,它会脱离终端执行,例如windows上的各种服务。

PID

PID process id : 进程号,在linux系统上进程都会被分配一个ID,这个ID就是进程的标号。在linux系统上所有的进程都可以在 /proc 目录下查看。PPID 是进程的父进程号。在一个系统上可以通过如下命令查看能创建的最大进程的个数:

1
cat /proc/sys/kernel/pid_max

特殊PID的进程

0号进程 idle : 在linux系统启动的时候最先运行的进程就是0号进程,0号进程又叫空闲进程。如果系统上没有其他进程执行那么0号进程就执行。0号进程是1号进程和2号进程的父进程

1号进程 init : init进程是由0号进程创建得到的,它的主要工作是系统的初始化。当初始化工作执行完之后,它主要负责回收孤儿进程的资源。

2号进程 kthreadd : kthreadd是有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
ps -ef # 查看进程的父子关系
# PID:进程号
# PPID:父进程号
# TTY:如果是问号,说明没有终端与之对应
# CMD:进程名

ps -ajx # 一般使用这条命令查看,查看到的进程的信息更完全
# PID:进程号
# PPID:父进程号
# PGID:进程组ID
# SID:会话ID
# 在linux系统上新开一个终端就会默认创建一个会话,一个会话包含多个进程组,其中进程组有分为前台进程组和后台进程组,前台进程组只有一个,后台进程组有多个。一个进程组内包含很多个进程,进程具备父子关系。
# TTY:如果是问号,说明没有终端与之对应
# TPGID:如果是-1就是守护进程

sudo apt-get install htop
top
htop
# htop动态查看进程信息比top查看的更可视化一些

kill -l # 给进程发信号的命令
kill -信号号 PID # 给PID进程发送信号
# SIGINT:打断正在执行的程序(ctrl+c)
# SIGKILL:杀死进程
# SIGSTOP:停止
# SIGCONT:继续

pidof FILE_NAME # 查看进程号 pidof a.out
killall FILE_NAME # 杀死有同名的进程 killall a.out

cat /proc/sys/kernel/pid_max # 查看最大进程号的命令

进程的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 进程的状态
* D 不可中断的等待态(sleep,不可被信号打断)
* R 运行状态
* S 可中断的等待态(sleep,可被信号打断)
* T 停止状态
* X 死亡状态
* Z 僵尸态
* I 空闲态

2. 进程的附加态
* < 高优先级进程
* N 低优先级进程
* L 锁在内存上
* s 会话组组长
* l 包含多线程
* + 前台进程

孤儿进程 :一个进程的父进程死亡,此时当前的进程就是孤儿进程,孤儿进程被init收养

僵尸态进程 :如果一个进程结束,父进程没有为它收尸,此时当前的进程就是僵尸进程。

进程状态切换实例

1
2
3
4
5
6
7
8
#include <head.h>
int main(int argc,const char * argv[])
{
while(1){
sleep(1);
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
./a.out # 运行上述程序程序状态(前台休眠)
ps -ajx | grep a.out # 查看进程状态

# ctrl + z # 将进程变为停止态
ps -ajx | grep a.out # 查看进程状态

jobs -l # 查看状态
bg 1 # 将停止态的进程变为后台休眠态
ps -ajx | grep a.out # 查看进程状态

fg 1 # 将后台休眠态的进程变为前台休眠态
ps -ajx | grep a.out # 查看进程状态

进程的创建及特点

如何创建进行

进程的创建是拷贝父进程得到的,通过拷贝过程更容易得到子进程,并且能够标识进程的父子状态。

创建进程的API

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

pid_t fork(void);
// 功能:创建一个子进程
// 参数:
// @无
// 返回值:成功父进程收到子进程的PID,子进程收到0
// 失败父进程收到-1,并置位错误码

创建进程的实例

创建一个子进程,不关注返回值

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

int main(int argc, const char* argv[])
{
fork(); // 创建一个子进程

while (1); // 父子进程都在执行while(1);

return 0;
}

创建进程的实例

创建多个子进程,不关注返回值

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

int main(int argc, const char* argv[])
{
for (int i = 0; i < 3; i++) {
fork(); // 创建一个子进程
}
while (1);

return 0;
}

按照上述的写法:fork n 次就产生了 2^n 个进程

可以分次总体的理解上述程序的执行过程

fork 1

也可以按照父子关系理解上述程序的执行过程

fork 2

fork缓冲区 结合问题

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

int main(int argc, const char* argv[])
{
for (int i = 0; i < 2; i++) {
fork();
printf("-");
}

return 0;
}

上述程序会打印8个 -

原因是上述程序printf没有刷新缓冲区,所以在fork进程的时候,会将父进程缓冲区的内容也fork过来,所以最终打印了8个’-‘

关注 fork 返回值

fork 的返回值是区分父子进程的关键

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

int main(int argc,const char * argv[])
{
pid_t pid;

//关注返回值可以让父子进程执行不同的代码区
pid = fork();
if(pid == -1){
PRINT_ERR("fork error");
}else if(pid == 0){
//子进程代码区
}else{
//父进程代码区
}

return 0;
}

父子进程执行先后顺序

父子进程执行没有先后顺序,时间片轮询,上下文切换。

父子进程内存空间问题

在fork前父进程中所有的变量多被拷贝到了子进程中,在子进程中打印的变量的虚拟地址和父进程一样,但是两者在物理地址上一定是不同的,所以在子进程内修改变量的值,父进程中变量不会改变。父子进程内存空间相互独立。

写时拷贝 cow(copy on write)
在使用fork产生子进程的时候,此时父子进程共用同一块物理内存,但是在子进程或者父进程中尝试修改a变量的时候,此时就会分配一块新的物理内存,此时父子进程a变量虚拟内存相同,但是对应的物理内存不同。

多进程练习

使用两个进程拷贝同一个文件,父进程拷贝文件的前一半,子进程拷贝文件的后一半。

1
./a.out srcfile destfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <head.h>
int get_file_len(const char* file)
{
int fd, len;
if ((fd = open(file, O_RDONLY)) == -1)
PRINT_ERR("open error");
len = lseek(fd, 0, SEEK_END);
close(fd);
return len;
}
int init_src_file(const char* file)
{
int fd;
if ((fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0666)) == -1)
PRINT_ERR("open error");

close(fd);
return 0;
}
int copy_file(const char* src, const char* dest, int start, int len)
{
char s[20];
int fd1, fd2;
int ret, count = 0;
// 1.以只读方式打开源文件,以只写方式打开目标文件
if ((fd1 = open(src, O_RDONLY)) == -1)
PRINT_ERR("open src error");
if ((fd2 = open(dest, O_WRONLY)) == -1)
PRINT_ERR("open dest error");
// 2.定位源和目标文件的光标
lseek(fd1, start, SEEK_SET);
lseek(fd2, start, SEEK_SET);
// 3.循环拷贝
// while (1) {
// ret = read(fd1, s, sizeof(s)); // 从源文件中读
// count += ret; // 将每次读的数据加到count中
// if (count >= len) {
// write(fd2, s, (ret - (count - len)));
// break;
// }
// write(fd2, s, ret);
// }
while (count < len) {
ret = read(fd1, s, sizeof(s)); // 从源文件中读
count += ret; // 将每次读的数据加到count中
write(fd2, s, ret);
}
// 4.关闭文件
close(fd1);
close(fd2);
return 0;
}

int main(int argc, const char* argv[])
{
int len;
pid_t pid;
// 1.检查参数个数
if (argc != 3) {
fprintf(stderr, "input error,try again\n");
fprintf(stderr, "usage:./a.out srcfile destfile\n");
return -1;
}
// 2.获取源文件大小
len = get_file_len(argv[1]);

// 3.创建出目标文件,并清空
init_src_file(argv[2]);

// 4.fork进程,拷贝文件
if ((pid = fork()) == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程
copy_file(argv[1], argv[2], len / 2, (len - len / 2));
} else {
// 父进程
copy_file(argv[1], argv[2], 0, len / 2);
wait(NULL);
}

return 0;
}

父子进程光标问题

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

int main(int argc, const char* argv[])
{
pid_t pid;
int fd;

// 如果文件是在fork前打开的,此时父子进程共用同一个光标,如果在父进程修改光标,子进程会受到影响。
// 如果不想让两个进程共用光标,可以在两个进程内分别打开。
if((fd = open("./hello.txt",O_RDWR))==-1)
PRINT_ERR("open error");

pid = fork();
if (pid == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程
lseek(fd,5,SEEK_SET);
} else {
//父进程
sleep(1);
char ch;
read(fd,&ch,1);
printf("ch = %c\n",ch);
}
close(fd);
return 0;
}

进程相关的API接口

getpid getppid 函数

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

pid_t getpid(void);
// 功能:获取当前进程进程号

pid_t getppid(void);
// 功能:获取父进程进程号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <head.h>

int main(int argc, const char* argv[])
{
pid_t pid;
// 关注返回值可以让父子进程执行不同的代码区
pid = fork();
if (pid == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程代码区
printf("child:pid = %d,ppid = %d\n",getpid(),getppid());
} else {
// 父进程代码区
printf("parent:pid = %d,ppid = %d,cpid = %d\n",getpid(),getppid(),pid);
}
return 0;
}

exit _exit 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void exit(int status);
// 功能:结束进程,它是库函数,当使用exit结束进程时会刷新缓冲区
// 参数:
// @status:进程退出的状态值 [0-255]
// EXIT_SUCCESS (0)
// EXIT_FAILURE (1)
// 返回值:无

void _exit(int status);
// 功能:结束进程,它是系统调用,当使用_exit结束进程时不会刷新缓冲区
// 参数:
// @status:进程退出的状态值 [0-255]
// EXIT_SUCCESS (0)
// EXIT_FAILURE (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
#include <head.h>
void func(void)
{
printf("111111111\n"); //打印
printf("2222222222"); //打印
exit(EXIT_SUCCESS); //进程退出,子进程变成了僵尸进程
printf("3333333333\n");//不会执行
}
int main(int argc, const char* argv[])
{
pid_t pid;
// 关注返回值可以让父子进程执行不同的代码区
pid = fork();
if (pid == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程代码区
func();
while(1); //不会执行
} else {
// 父进程代码区
while (1);
}
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
#include <head.h>
void func(void)
{
printf("111111111\n"); //打印
printf("2222222222"); //执行了,但是不会打印到终端,因为没刷新缓冲区
_exit(EXIT_SUCCESS); //退出进程,进程变成僵尸进程
printf("3333333333\n");//没有执行
}
int main(int argc, const char* argv[])
{
pid_t pid;

// 关注返回值可以让父子进程执行不同的代码区
pid = fork();
if (pid == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程代码区
func();
while(1); //没有执行
} else {
// 父进程代码区
while (1);
}
return 0;
}

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

pid_t wait(int *wstatus);
// 功能:在父进程中调用wait回收子进程的资源(阻塞等待子进程结束)
// 参数:
// @wstatus:接收到子进程_exit/exit退出的状态值
// 返回值:成功返回回收掉资源的进程号,失败返回-1,置位错误码

wait(NULL); //回收子进程的资源,但不关注子进程退出状态

int wstatus;
wait(&wstatus);
// WIFEXITED(wstatus): 如果进程遇到exit/_exit/return正常退出,这个宏返回真
// WEXITSTATUS(wstatus): 获取wstatus中bit8-bit15这8个bit,代表子进程退出的状态
// WIFSIGNALED(wstatus): 如果是信号导致进程退出,这个宏返回真
// WTERMSIG(wstatus): 获取信号号,信号号对应的是wstatus中bit0-bit6这7个bit位

pid_t waitpid(pid_t pid, int *wstatus, int options);
// 功能:指定回收pid号进程的资源
// 参数:
// @pid:进程号
// < -1 回收pid绝对值同组的任意的子进程的资源
// -1 回收任意子进程的资源
// 0 回收和调用进程同组的子进程的资源
// > 0 表示回收pid对应的子进程的资源
// @wstatus:收到调用的子进程退出的状态
// @options:
// 0 :阻塞回收
// WNOHANG :非阻塞回收
// 返回值:成功返回回收掉的子进程的pid
// 如果是非阻塞,没有回收掉子进程返回0
// 如果失败返回-1置位错误码

// wait(NULL) === 等价于 == waitpid(-1,NULL,0)
// wait(&wstatus) === 等价于 == waitpid(-1,&wstatus,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
#include <head.h>
void func(void)
{
printf("111111111\n");
printf("2222222222");
// while(1);
_exit(34);
printf("3333333333\n");
}
int main(int argc, const char* argv[])
{
pid_t pid;

// 关注返回值可以让父子进程执行不同的代码区
pid = fork();
if (pid == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程代码区
func();
while(1);
} else {
// 父进程代码区
int wstatus;
pid_t pid1;
if((pid1 = wait(&wstatus))==-1)
PRINT_ERR("wait error");

printf("pid = %d,pid1 = %d\n",pid,pid1);
if(WIFEXITED(wstatus)){ //如果为真是正常退出的
printf("status = %d\n",WEXITSTATUS(wstatus));//获取wstatus中bit8-bit15
}
if(WIFSIGNALED(wstatus)){//信号导致子进程退出
printf("signo = %d\n", WTERMSIG(wstatus)); //获取wstatus中bit0-bit6
}
while(1);
}
return 0;
}

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
#include <head.h>
void func(void)
{
printf("111111111\n");
printf("2222222222");
sleep(1);
printf("我是第一个子进程\n");
// while (1);
_exit(34);
printf("3333333333\n");
}
int main(int argc, const char* argv[])
{
pid_t pid;

// 关注返回值可以让父子进程执行不同的代码区
pid = fork();
if (pid == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 子进程代码区
func();
while (1)
;
} else {
pid_t pid1;
if ((pid1 = fork()) == -1) {
PRINT_ERR("fork error");
} else if (pid1 == 0) {
sleep(1);
printf("我是第二个子进程\n");
exit(0);
}

// 父进程代码区
int wstatus;
pid_t pid2;
if ((pid2 = waitpid(-1, &wstatus, WNOHANG)) == -1)
PRINT_ERR("wait error");

printf("pid = %d,pid2 = %d\n", pid, pid2);
if (WIFEXITED(wstatus)) { // 如果为真是正常退出的 ,bit8-bit15
printf("status = %d\n", WEXITSTATUS(wstatus));
}
if (WIFSIGNALED(wstatus)) {
printf("signo = %d\n", WTERMSIG(wstatus)); // bit0-bit6
}
while (1);
}
return 0;
}

进程间的通信

进程间通信方式简介

在linux系统上常用的进程间通信方式有如下7种:

  • 传统进程间通信
    • 无名管道
    • 有名管道
    • 信号
  • System V IPC进程间通信
    • 消息队列
    • 共享内存
    • 信号量(信号灯集)
  • BSD(伯克利分校)基于网络的进程间通信
    • socket 实现进程间通信

无名管道

无名管道通信原理

如果A和B进程想要通过无名管道通信,那就必须在内核空间创建一个无名管道 (64K), A和B进程必须是亲缘关系的进程,A进程向管道的一端写数据,B进程可以从管道的另外一端读数据。在A进程和B进程进行数据传输的时候是不允许使用 lseek 函数的。无名管道是 半双工 的通信方式。
如果A进程一直向管道中写数据写满 64K 的时候A进程阻塞,直到B进程读一部分数据之后A才能继续写。
如果B进程在读数据的时候,无名管道是空的,B进程阻塞。

无名管道的API

1
2
3
4
5
6
7
8
9
#include <unistd.h>

int pipe(int pipefd[2]);
// 功能:创建一个无名管道
// 参数:
// @pipefd:返回管道的两端
// pipefd[1]:写端
// pipefd[0]:读端
// 返回值:成功返回0,失败返回-1置位错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <head.h>

int main(int argc, const char* argv[])
{
pid_t pid;
int pipefd[2];
char s[128] = { 0 };
// 1.创建无名管道
if (pipe(pipefd))
PRINT_ERR("pipe error");

// 2.创建父子进程
if ((pid = fork()) == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
close(pipefd[1]); // 关闭子进程的写端
// 子进程
while (1) {
// 清空s
memset(s,0,sizeof(s));
// 从管道向s中读数据
read(pipefd[0], s, sizeof(s));
// 如果读到的是quit就退出
if (strcmp(s, "quit") == 0)
break;
// 将读取到的数据打印到终端上
printf("%s\n", s);
}
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
close(pipefd[0]); // 关闭父进程的读端
// 父进程
while (1) {
// 从终端向s数组读取字符串
fgets(s, sizeof(s), stdin);
// 清除换行符
if (s[strlen(s) - 1] == '\n')
s[strlen(s) - 1] = '\0';
// 向管道中写数据
write(pipefd[1], s, strlen(s));
// 如果输入的是quit让进程退出
if (strcmp(s, "quit") == 0)
break;
}
close(pipefd[1]);
//等待回收子进程的资源
wait(NULL);
}
return 0;
}

无名管道通信特点

读端存在写管道:有多少写多少,直到写满(64K)为止,写阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <head.h>

int main(int argc, const char* argv[])
{
int pipefd[2];
char s[128] = { 0 };

// 1.创建无名管道
if (pipe(pipefd))
PRINT_ERR("pipe error");
// 2.读端存在,但是没有读数据,写管道
// 管道的大小是64K(65536),如果写65536个字节,写没有阻塞,printf("1111111...")会打印
// 如果将循环的次数改为65537的时候,最后一次写,写阻塞,所以printf("11111...")不会打印
// 说明写满了
char ch='a';
for(int i=0;i<65537;i++){
write(pipefd[1],&ch,1);
}

printf("11111111111111111111111111\n");
return 0;
}

读端不存在写管道:管道破裂,进程收到SIGPIPE信号,进程退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <head.h>

int main(int argc, const char* argv[])
{
int pipefd[2];
char s[128] = { 0 };

// 1.创建无名管道
if (pipe(pipefd))
PRINT_ERR("pipe error");

close(pipefd[0]); //关闭读端

char ch='a';
//关闭读端,写管道,管道破裂,操作系统给当前进程发送SIGPIPE,
// 将当前进程杀死
write(pipefd[1],&ch,1);

while(1);

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
#include <head.h>

int main(int argc, const char* argv[])
{
int pipefd[2];
char s[128] = "i am test pipe func...\n";

// 1.创建无名管道
if (pipe(pipefd))
PRINT_ERR("pipe error");

write(pipefd[1],s,strlen(s));

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

char ch;
while(1){
read(pipefd[0],&ch,1);
printf("ch = %c\n",ch);
usleep(100000);
}
return 0;
}

总结:

  • 读端存在写管道:有多少写多少,直到写满(64K)为止,写阻塞
  • 读端不存在写管道:管道破裂,进程收到SIGPIPE信号,进程退出
  • 写端存在读管道:管道中有多少字节的数据就能读多少数据,如果没有数据的时候,读阻塞
  • 写端不存在读管道:管道中有多少字节的数据就能读多少数据,如果没有数据的时候,读立即返回

有名管道

有名管道通信原理

有名管道可以实现任意进程间的通信,有名管道的大小也是 64K,有名管道也是不支持 lseek,有名管道也是 半双工 的通信方式。有名管道创建之后会在 用户空间产生一个管道文件,这个管道文件是在内存上存储的。
如果A和B两个进程想要通过有名管道通信,就打开管道文件,向管道中写向管道中读就可

有名管道的API

1
2
3
4
5
6
int mkfifo(const char *pathname, mode_t mode);
// 功能:创建有名管道
// 参数:
// @pathname:管道文件的路径及名字
// @mode:管道文件的操作权限(mode & ~umask)
// 返回值:成功返回0,失败返回-1置位错误码

01mkfifo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <head.h>
#define FIFO_NAME "./myfifo"
int main(int argc,const char * argv[])
{
// 1.创建管道
if(mkfifo(FIFO_NAME,0666))
PRINT_ERR("mkfifo error");

// 2.等待用户使用
getchar();

// 3.销毁管道
char s[50] = {0};
snprintf(s,sizeof(s),"rm -rf %s",FIFO_NAME);
system(s);

return 0;
}

02write.c

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 <head.h>
#define FIFO_NAME "./myfifo"
int main(int argc, const char* argv[])
{
int fd;
char s[128] = { 0 };
if ((fd = open(FIFO_NAME, O_WRONLY)) == -1)
PRINT_ERR("open error");

while (1) {
printf("input > ");
// 从终端向s数组读取字符串
fgets(s, sizeof(s), stdin);
// 清除换行符
if (s[strlen(s) - 1] == '\n')
s[strlen(s) - 1] = '\0';
// 向管道中写数据
write(fd, s, strlen(s));
// 如果输入的是quit让进程退出
if (strcmp(s, "quit") == 0)
break;
}

close(fd);

return 0;
}

03read.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <head.h>
#define FIFO_NAME "./myfifo"
int main(int argc, const char* argv[])
{
int fd;
char s[128] = { 0 };
if ((fd = open(FIFO_NAME, O_RDONLY)) == -1)
PRINT_ERR("open error");

while (1) {
memset(s, 0, sizeof(s));
read(fd, s, sizeof(s));
printf("s = %s\n", s);
if (strcmp(s, "quit") == 0)
break;
}

close(fd);

return 0;
}

有名管道的练习

使用有名管道传输文件,
A进程读文件,将文件中的内容写入到管道中
B进程读取管道,将管道中的数据写入到新文件中。

01mkfifo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <head.h>
#define FIFO_NAME "./myfifo"
int main(int argc,const char * argv[])
{
// 1.创建管道
if(mkfifo(FIFO_NAME,0666))
PRINT_ERR("mkfifo error");

// 2.等待用户使用
getchar();

// 3.销毁管道
char s[50] = {0};
snprintf(s,sizeof(s),"rm -rf %s",FIFO_NAME);
system(s);

return 0;
}

02write.c

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 <head.h>
#define FIFO_NAME "./myfifo"
int main(int argc, const char* argv[])
{
int fd1, fd2, ret;
char s[128] = { 0 };
// 校验参数
if (argc != 2) {
fprintf(stderr, "input error,try again\n");
fprintf(stderr, "usgage: ./a.out srcfile\n");
return -1;
}
// 打开源文件
if ((fd1 = open(argv[1], O_RDONLY)) == -1)
PRINT_ERR("open error");
// 打开管道文件
if ((fd2 = open(FIFO_NAME, O_WRONLY)) == -1)
PRINT_ERR("open error");

// 循环读写(从源文件读,向管道写)
while ((ret = read(fd1, s, sizeof(s))) > 0) {
write(fd2,s,ret);
}

close(fd1);
close(fd2);

return 0;
}

03read.c

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 <head.h>
#define FIFO_NAME "./myfifo"
int main(int argc, const char* argv[])
{
int fd1, fd2, ret;
char s[128] = { 0 };
// 校验参数
if (argc != 2) {
fprintf(stderr, "input error,try again\n");
fprintf(stderr, "usgage: ./a.out destfile\n");
return -1;
}
// 打开目标文件
if ((fd1 = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC,0666)) == -1)
PRINT_ERR("open error");
// 打开管道文件
if ((fd2 = open(FIFO_NAME, O_RDONLY)) == -1)
PRINT_ERR("open error");

// 循环读写(从源文件读,向管道写)
while ((ret = read(fd2, s, sizeof(s))) > 0) {
write(fd1,s,ret);
}

close(fd1);
close(fd2);

return 0;
}

有名管道读写的特点

  • 读端存在写管道有多少写多少,直到写满为止(64K),写阻塞

  • 读端没有打开过,写管道写端在open位置阻塞

  • 读端先打开后关闭,写管道管道破裂,收到SIGPIPE信号,进程结束

  • 写端存在读管道有多少读多少,没数据读的时候读阻塞

  • 写端没有打开过,读管道读端在open的位置阻塞

  • 写端先打开后关闭,读管道有多少读多少,没数据的时候读立即返回(读返回值是0)

信号

信号简介

用户可以通过 kill 命令给进程发信号,操作系统也可以给进程发送信号。进程对信号的响应方式有三种:忽略,默认,捕捉

发信号的命令

1
kill -信号号  PID

信号的查看方式

1
kill -l

常用的信号

信号名 默认操作 说明
SIGHUP 终止 该信号在用户终端关闭时产生,通常是发给和该终端关联的会话内的所有进程
SIGINT 终止 该信号在用户键入 INTR 字符 ctrl + c 时产生,内核发送此信号发送到当前终端的所有前台进程
SIGQUIT 终止 该信号和 SIGINT 类似,但由 UIT 字符 Ctrl + \
SIGILL 终止 该信号在一个进程企图执行一条非法指令时产生
SIGSEV 终止 该信号在非法访问内存时产生,如野指针、缓冲区溢出
SIGPIPR 终止 当进程往一个没有读端的管道中写入时产生,代表 管道破裂
SIGKILL 终止 该信号用于立即结束程序的运行,不能被捕捉或忽略
SIGSTOP 暂停进程 该信号用于立即暂停程序的运行,不能被捕捉或忽略
SIGTSTP 暂停进程 该信号用于暂停进程,在用户键入 SUSP 字符 Ctrl + z 时产生
SIGCONT 继续运行 该信号用于继续运行进程
SIGALRM 终止 该信号在调用 alarm 函数设置的定时器超时时产生
SIGUSR1/2 终止 该信号由用户使用

SIGCHLD : 当子进程退出的时候,父进程会收到这个信号

注:在所有的信号中,只有SIGKILL/SIGSTOP两个信号不能被捕捉,也不能被忽略。只能执行默认的动作。

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);
// 功能:给进程对信号指定处理方式
// 参数:
// @signum:信号号
// @handler:处理方式
// SIG_IGN:忽略
// SIG_DFL:默认
// handle:捕捉
// void handle(int signo)
// {
//
// }
// 返回值:成功返回handler,失败返回SIG_ERR,置位错误码
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 <head.h>
void signal_handle(int signo)
{
if(signo == SIGINT){
printf("我收到了一个ctrl+c信号\n");
}
}
int main(int argc, const char* argv[])
{
// 1.对SIGINT忽略
// if(SIG_ERR == signal(SIGINT,SIG_IGN))
// PRINT_ERR("signal error");

// 2.对SIGINT默认
// if(SIG_ERR == signal(SIGINT,SIG_DFL))
// PRINT_ERR("signal error");

// 3.对SIGINT捕捉
if (SIG_ERR == signal(SIGINT, signal_handle))
PRINT_ERR("signal error");

while (1);
return 0;
}

练习

使用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
#include <head.h>
void handle(int signo)
{
switch (signo) {
case SIGPIPE:
printf("我捕捉到了一个管道破裂信号\n");
break;
case SIGINT:
printf("我捕捉到了一个ctrl+c信号\n");
break;
}
}
int main(int argc, const char* argv[])
{
int pipefd[2];
char s[128] = { 0 };

if ((SIG_ERR == signal(SIGINT, handle)))
PRINT_ERR("signal error");

if ((SIG_ERR == signal(SIGPIPE, handle)))
PRINT_ERR("signal error");

if (pipe(pipefd))
PRINT_ERR("pipe error");

close(pipefd[0]);

char ch = 'a';
while (1) {
write(pipefd[1], &ch, 1);
sleep(1);
}

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
#include <head.h>
char cmd[128] = {0};
void handle(int signo)
{
waitpid(-1,NULL,WNOHANG);
printf("我是父进程,我以非阻塞方式回收掉了子进程的资源\n");
snprintf(cmd,sizeof(cmd),"kill -%d %d",SIGUSR1,getpid());
system(cmd);
}
int main(int argc, const char* argv[])
{
pid_t pid;

if ((pid = fork()) == -1) {
PRINT_ERR("fork error");
}else if(pid == 0){
sleep(7);
printf("我是子进程,我执行了7s,我现在要退出...\n");
exit(EXIT_SUCCESS);
}else{
if(SIG_ERR == signal(SIGCHLD,handle))
PRINT_ERR("signal error");
while(1);
}
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
int raise(int sig);
// 功能:给自己发信号
// 参数:
// @sig:信号号
// 返回值:成功返回0,失败返回非0

int kill(pid_t pid, int sig);
// 功能:给指定pid的进程发送信号
// 参数:
// @pid:进程号
// pid > 0 :给pid号的进程发信号
// pid = 0 :给同组的进程发送信号
// pid = -1:给所有有权限的进程发送信号
// pid <-1:首先会对pid取绝对值,给和这个绝对值相同的组的进程发送信号
// @信号号
// 返回值:成功返回0,失败返回-1置位错误码

unsigned int alarm(unsigned int seconds);
// 功能:当seconds倒计时为0的时候发送SIGALRM信号
// 参数:
// @seconds:秒钟数,如果填写为0,取消挂起的信号
// 返回值:如果alarm是第一次调用,返回0.
// 如果alarm不是第一次调用,返回上一次调用的剩余秒钟数

// alarm(5); //返回值是0
// sleep(2); //延时2s
// alarm(5); //返回值是3

raise / 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
#include <head.h>

void handle(int signo)
{
waitpid(-1,NULL,WNOHANG);
printf("我是父进程,我以非阻塞方式回收掉了子进程的资源\n");
//raise(SIGUSR1);
kill(getpid(),SIGUSR1);
}
int main(int argc, const char* argv[])
{
pid_t pid;

if ((pid = fork()) == -1) {
PRINT_ERR("fork error");
}else if(pid == 0){
sleep(7);
printf("我是子进程,我执行了7s,我现在要退出...\n");
exit(EXIT_SUCCESS);
}else{
if(SIG_ERR == signal(SIGCHLD,handle))
PRINT_ERR("signal error");
while(1);
}
return 0;
}

alarm

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

void handle(int signo)
{
printf("我收到了闹钟信号\n");
}
int main(int argc,const char * argv[])
{
if(SIG_ERR == signal(SIGALRM,handle))
PRINT_ERR("signal error");

printf("第一次 = %d\n",alarm(5));
sleep(2);
printf("第二次 = %d\n",alarm(5)); //SIGALRM

while(1);
return 0;
}

alarm 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <head.h>
void handle(int signo)
{
printf("系统自动出牌了\n");
alarm(4);
}
int main(int argc,const char * argv[])
{
if(SIG_ERR == signal(SIGALRM,handle))
PRINT_ERR("signal error");

alarm(4);
char ch;
while(1){
ch = getchar();
getchar(); //吃'\n'
printf("用户出牌 = %c\n",ch);
alarm(4);
}
return 0;
}

IPC进程间通信相关命令

1
2
3
4
5
6
ipcs -q     #查看消息队列
ipcs -m #查看共享内存
ipcs -s #查看信号灯集
ipcrm -q msqid #删除消息队列
ipcrm -m shmid #删除共享内存
ipcrm -s semid #删除信号灯集

IPC进程间通信键值获取及组成

在使用IPC进程间通信的时候,首先需要获取一个key,只有当两个进程拿到相同的键的之后才能找到同一个IPC。

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

key_t ftok(const char *pathname, int proj_id);
// 功能:获取一个键值(键不是唯一的)
// 参数:
// @pathname:路径及名字
// @proj_id:只有低8bit有效
// 返回值:成功返回键值,失败返回-1置位错误码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <head.h>

int main(int argc, const char* argv[])
{
key_t key;
struct stat st;

if ((key = ftok("/home/linux", 't')) == -1)
PRINT_ERR("ftok error");

printf("key = %#x\n", key);

if (stat("/home/linux", &st))
PRINT_ERR("stat error");

printf("inode = %#lx,devno = %#lx,proj_id = %#x\n",st.st_ino,st.st_dev,'t');
return 0;
}

消息队列

消息队列通信原理

如果想要使用消息队列实现进程间通信,就必须在内核空间创建出来消息队列,消息队列默认大小是 16384(16K),当创建好消息队列之后A进程可以向消息队列中发消息,消息的格式是 类型 + 正文。当消息队列满的时候A进程如果还想往消息队列中发消息A进程休眠。B进程可以通过消息的类型从消息队列中取消息,取出的消息从队列中移除。
如果B进程想要获取的消息类型在队列中不存在B进程休眠等。

A进程向消息队列中发消息的时候可以采用:阻塞,非阻塞方式
B进程从消息队列中收消息的时候可以采用:阻塞,非阻塞方式

msgget msgsnd msgrcv msgctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
// 功能:创建消息队列
// 参数:
// @key:键值
// key:通过ftok获取
// IPC_PRIVATE:只能用于亲缘间进程的通信
// @msgflag:消息队列的标志位
// IPC_CREAT|0666 或 IPC_CREAT|IPC_EXCL|0666
// 返回值:成功返回消息队列号,失败返回-1置位错误码

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 功能:向消息队列中发消息
// 参数:
// @msqid:消息队列号
// @msgp:消息的首地址
// struct msgbuf {
// long mtype; //消息的类型,必须大于0
// char mtext[255]; //消息的正文
// };
// @msgsz:消息正文的大小
// @msgflg:消息的标志
// 0:阻塞发送
// IPC_NOWAIT:非阻塞发送
// 返回值:成功返回0,失败返回-1置位错误码

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
// 功能:从消息队列获取消息
// 参数:
// @msqid:消息队列号
// @msgp:消息的首地址
// @msgsz:消息正文的大小
// @msgtyp:消息的类型
// 如果=0,接收消息队列中的第一个消息
// 如果>0 ,接收msgtyp指定的消息类型
// 如果<0,那么将读取队列中第一个最小类型小于或等于msgtyp绝对值的消息。
// 2-3-100-500-30-2000
// -100===>100
// 2-3-100-30
// @msgflg:消息的标志
// 0:阻塞接收
// IPC_NOWAIT:非阻塞接收
// 返回值:失败返回-1置位错误码。成功返回接收接收的字节的个数

01snd.c

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 <head.h>
typedef struct {
long mtype;
char name[19];
char sex;
int age;
} msg_t;

#define MSGSIZE (sizeof(msg_t)-sizeof(long))

int main(int argc, const char* argv[])
{
key_t key;
int msgqid;
// 1.获取键值
if ((key = ftok("/home/linux/", 'p')) == -1)
PRINT_ERR("ftok error");

// 2.创建消息队列
if ((msgqid = msgget(key, IPC_CREAT | 0666)) == -1)
PRINT_ERR("msgget error");

// 3.向消息队列中发消息
int ret;
msg_t msg;
while (1) {
retry:
printf("input (type name sex age) > ");
ret = scanf("%ld %s %c %d", &msg.mtype, msg.name, &msg.sex, &msg.age);
if (ret != 4) {
printf("input error,try again\n");
while (getchar() != '\n');
goto retry;
}

if(msgsnd(msgqid, &msg,MSGSIZE, 0))
PRINT_ERR("msgsnd error");

if(msg.mtype == 1000) break;
}

return 0;
}

02rcv.c

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 <head.h>
typedef struct {
long mtype;
char name[19];
char sex;
int age;
} msg_t;

#define MSGSIZE (sizeof(msg_t)-sizeof(long))

int main(int argc, const char* argv[])
{
key_t key;
int msgqid;
// 1.获取键值
if ((key = ftok("/home/linux/", 'p')) == -1)
PRINT_ERR("ftok error");

// 2.创建消息队列
if((msgqid = msgget(key,IPC_CREAT|0666))==-1)
PRINT_ERR("msgget error");

// 3.接收消息
long type;
msg_t msg;
while(1){
printf("input (type) > ");
scanf("%ld",&type);

if(msgrcv(msgqid,&msg,MSGSIZE,type,0)==-1)
PRINT_ERR("msgrcv error");

printf("type=%ld,name=%s,sex=%c,age=%d\n",msg.mtype,msg.name,msg.sex,msg.age);
if(msg.mtype == 1000) break;
}
if(msgctl(msgqid,IPC_RMID,NULL)) //删除消息队列
PRINT_ERR("msgctl error");
return 0;
}

msgctl 函数详解

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
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 功能:消息队列的控制
// 参数:
// @msqid:消息队列号
// @cmd:命令码
// IPC_STAT:获取消息队列的属性(ipcs -q msqid)
// IPC_SET:设置消息队列的属性(如设置消息队列大小)
// IPC_RMID:立即删除消息队列,唤醒所有等待的读取器和写入(ipcrm -q msqid)
// 器进程(返回一个错误并将errno设置为EIDRM)。调用进程
// 必须具有适当的特权,或者它的有效用户ID必须是消息队
// 列的创建者或所有者的ID。在这种情况下,msgctl()的/*第
// 三个参数将被忽略*/。
// @buf:msqid_ds消息队列属性结构体
// 返回值:成功返回0,失败返回-1,置位错误码

// eg1:使用msgctl删除消息队列
msgctl(msqid,IPC_RMID,NULL);
// eg2:获取消息队列属性
struct msqid_ds msqds;
msgctl(msqid, IPC_STAT, &msqds); //获取到的属性在msqds结构体中存放

//以下是对msqid_ds结构体的详解
struct msqid_ds
{
struct ipc_perm msg_perm; //权限结构体
__time_t msg_stime; //最后一次发送消息的时间
__time_t msg_rtime; //最后一次接收消息的时间
__syscall_ulong_t __msg_cbytes; //当前消息队列中字节数
msgqnum_t msg_qnum; //当前消息队列中消息的个数
msglen_t msg_qbytes; //消息队列中能够容纳的字节数(16384)
__pid_t msg_lspid; //最后一次发送消息的进程号
__pid_t msg_lrpid; //最后一次接收消息的进程号
};
struct ipc_perm
{
__key_t __key; //键值
__uid_t uid; //消息队列所属的uid
__gid_t gid; //消息队列所属的gid
__uid_t cuid; //创建消息队列的uid
__gid_t cgid; //创建消息队列的gid
unsigned short int mode; //消息队列的读写权限
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <head.h>
typedef struct {
long mtype;
char name[19];
char sex;
int age;
} msg_t;

#define MSGSIZE (sizeof(msg_t) - sizeof(long))

int main(int argc, const char* argv[])
{
key_t key;
int msgqid;
// 1.获取键值
if ((key = ftok("/home/linux/", 'p')) == -1)
PRINT_ERR("ftok error");

// 2.创建消息队列
if ((msgqid = msgget(key, IPC_CREAT | 0666)) == -1)
PRINT_ERR("msgget error");

// 3.向消息队列中发消息
int ret;
msg_t msg;
while (1) {
retry:
printf("input (type name sex age) > ");
ret = scanf("%ld %s %c %d", &msg.mtype, msg.name, &msg.sex, &msg.age);
if (ret != 4) {
printf("input error,try again\n");
while (getchar() != '\n')
;
goto retry;
}

if (msgsnd(msgqid, &msg, MSGSIZE, 0))
PRINT_ERR("msgsnd error");

if (msg.mtype == 1000)
break;
}

// 4.获取消息队列属性
struct msqid_ds msqds;
if (msgctl(msgqid, IPC_STAT, &msqds))
PRINT_ERR("msgctl error");

printf("key=%#x,uid=%d,gid=%d,mode=%#o,nq=%ld,nb=%ld,mb=%ld\n",
msqds.msg_perm.__key, msqds.msg_perm.uid, msqds.msg_perm.gid,
msqds.msg_perm.mode, msqds.msg_qnum, msqds.__msg_cbytes, msqds.msg_qbytes);

// 5.设置消息队列属性
msqds.msg_qbytes = 32768;
if (msgctl(msgqid, IPC_SET, &msqds))
PRINT_ERR("msgctl error");

// 6.再次获取消息队列属性
memset(&msqds, 0, sizeof(msqds));
if (msgctl(msgqid, IPC_STAT, &msqds))
PRINT_ERR("msgctl error");

printf("key=%#x,uid=%d,gid=%d,mode=%#o,nq=%ld,nb=%ld,mb=%ld\n",
msqds.msg_perm.__key, msqds.msg_perm.uid, msqds.msg_perm.gid,
msqds.msg_perm.mode, msqds.msg_qnum, msqds.__msg_cbytes, msqds.msg_qbytes);

// 7.删除消息队列
if (msgctl(msgqid, IPC_RMID, NULL))
PRINT_ERR("msgctl error");
return 0;
}

共享内存

共享内存的工作原理

共享内存是所有进程间通信方式中效率最高的一个,因为当创建共享内存之后,需要通信的A和B进程可以直接操作这块物理内存空间 ,省去了向内核拷贝数据的过程。共享内存的大小是 4K 的整数倍。

shmget shmat shmdt shmctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
// 功能:创建共享内存
// 参数:
// @key:键值
// key:通过ftok获取
// IPC_PRIVATE:只能用于亲缘间进程的通信
// @size:共享内存的大小 4k整数倍
// @msgflag:共享的标志位
// IPC_CREAT|0666 或 IPC_CREAT|IPC_EXCL|0666
// 返回值:成功返回共享内存编号,失败返回-1置位错误码

void *shmat(int shmid, const void *shmaddr, int shmflg);
// 功能:映射共享内存到当前的进程空间
// 参数:
// @shmid:共享内存的编号
// @shmaddr:NULL,让系统自动分配
// @shmflg:共享内存的操作方式
// 0:读写
// SHM_RDONLY:只读
// 返回值:成功返回共享内存的首地址,失败返回(void *)-1,并置位错误码

int shmdt(const void *shmaddr);
// 功能:取消地址映射
// 参数:
// @shmaddr:指向共享内存的指针
// 返回值:成功返回0,失败返回-1置位错误码

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 功能:共享内存控制的函数
// 参数:
// @shmid:共享内存的编号
// @cmd:操作的命令码
// IPC_STAT :获取
// IPC_SET:设置
// IPC_RMID:删除共享内存
// 标记要销毁的段。实际上,只有在最后一个进程将其分离之后
// (也就是说,关联结构shmid_ds的shm_nattch成员为零时),
// 段才会被销毁。调用者必须是段的所有者或创建者,或具有特权。buf参数被忽略。
// @buf:共享内存属性结构体指针
// 返回值:成功返回0,失败返回-1置位错误码

01write.c

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 <head.h>
#define SHMSIZE (4096)
int main(int argc, const char* argv[])
{
key_t key;
int shmid;
char* waddr;
// 1.获取键值 ftok
if ((key = ftok("/home", 'g')) == -1)
PRINT_ERR("ftok error");
// 2.创建共享内存 shmget
if ((shmid = shmget(key, SHMSIZE, IPC_CREAT | 0666)) == -1)
PRINT_ERR("shmget error");
// 3.将共享内存映射到用户空间 shmat
if ((waddr = shmat(shmid, NULL, 0)) == (void*)-1)
PRINT_ERR("shmat error");
// 4.共享内存操作(写)
while (1) {
printf("input > ");
fgets(waddr, SHMSIZE, stdin);
if (waddr[strlen(waddr) - 1] == '\n')
waddr[strlen(waddr) - 1] = '\0';
if (strcmp(waddr, "quit") == 0)
break;
}
// 5.取消映射 shmdt
if(shmdt(waddr))
PRINT_ERR("shmdt error");
return 0;
}

02read.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <head.h>
#define SHMSIZE (4096)
int main(int argc, const char* argv[])
{
key_t key;
int shmid;
char* raddr;
// 1.获取键值 ftok
if ((key = ftok("/home", 'g')) == -1)
PRINT_ERR("ftok error");
// 2.创建共享内存 shmget
if ((shmid = shmget(key, SHMSIZE, IPC_CREAT | 0666)) == -1)
PRINT_ERR("shmget error");
// 3.将共享内存映射到用户空间 shmat
if ((raddr = shmat(shmid, NULL, 0)) == (void*)-1)
PRINT_ERR("shmat error");
// 4.从共享内存中数数据
while (1) {
getchar(); // 敲回车读一次数据,如果不写这句话会疯狂刷屏
printf("read:%s\n", raddr);
if (strncmp(raddr, "quit", 4) == 0)
break;
}
// 5.取消映射 shmdt
if (shmdt(raddr))
PRINT_ERR("shmdt error");
// 6.删除共享内存 shmctl
if (shmctl(shmid, IPC_RMID, NULL))
PRINT_ERR("shmctl error");
return 0;
}

shmctl 函数详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 功能:共享内存控制的函数
// 参数:
// @shmid:共享内存的编号
// @cmd:操作的命令码
// IPC_STAT :获取 (ipcs -m)
// IPC_SET:设置
// IPC_RMID:删除共享内存 (ipcrm -m shmid)
// 标记要销毁的段。实际上,只有在最后一个进程将其分离之后
// (也就是说,关联结构shmid_ds的shm_nattch成员为零时),
// 段才会被销毁。调用者必须是段的所有者或创建者,或具有特权。buf参数被忽略。
// @buf:共享内存属性结构体指针
// 返回值:成功返回0,失败返回-1置位错误码

// eg1:
shmctl(shmid,IPC_RMID,NULL); //删除共享内存
// eg2:
struct shmid_ds buf;
shmctl(shmid,IPC_STAT,&buf); //获取消息队列的属性

//以下是shmid_ds结果的详解
struct shmid_ds
{
struct ipc_perm shm_perm; //权限结构体
size_t shm_segsz; //共享内存的大小,单位是字节
__time_t shm_atime; //最后一次调用shmat的时间
__time_t shm_dtime; //最后一次调用shmdt的时间
__time_t shm_ctime; //最后一次调用shmctl改变属性的时间
__pid_t shm_cpid; //创建共享内存的PID
__pid_t shm_lpid; //最后一次操作共享内存的PID
shmatt_t shm_nattch; //当前多少个进程关联共享内存
};
struct ipc_perm
{
__key_t __key; //键值
__uid_t uid; //消息队列所属的uid
__gid_t gid; //消息队列所属的gid
__uid_t cuid; //创建消息队列的uid
__gid_t cgid; //创建消息队列的gid
unsigned short int mode; //消息队列的读写权限
};

信号灯集

信号灯集工作原理

信号量(信号灯集)是实现 进程同步 的机制,在一个信号灯集中可以有很多个信号灯。在信号灯集内信号灯相互独立,每个灯的值的改变不会影响其他的信号灯,信号灯的值一般设置为二值量( 1 或者 01 代表有资源,0 代表没有资源)。

semget semctl semop

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

int semget(key_t key, int nsems, int semflg);
// 功能:创建一个信号灯集
// 参数:
// @key:键值
// IPC_PRIVATE
// key
// @nsems:信号灯集合中信号灯的个数
// @semflag:创建的标志位
// IPC_CREAT|0666 或 IPC_CREAT|IPC_EXCL|0666
// 返回值:成功返回semid,失败返回-1置位错误码

int semctl(int semid, int semnum, int cmd, ...);
// 功能:信号灯集的控制函数
// 参数:
// @semid信号灯集的ID
// @senum:信号灯的编号
// @cmd:命令码
// SETVAL:设置信号灯的值 --->第四个参数val选项
// GETVAL:获取信号灯的值 --->不需要第四个参数
// IPC_STAT:获取信号灯集的属性--->第二个参数被忽略,第四个参数buf选项
// IPC_SET :设置信号灯集的属性--->第四个参数buf选项
// IPC_RMID:第二参数被忽略,第4个参数不用填写
// @...:
// union semun {
// int val; /* Value for SETVAL */
// struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
// };
// 返回值:失败返回-1置位错误码
// 成功:
// GETVAL:成功返回信号灯的值
// 其余的命令码成功返回0

// eg:设置灯的值
union semun sem = {
.val = 1,
}
semctl(semid,0,SETVAL,sem); //将0号灯初始值设置为1
union semun sem = {
.val = 0,
}
semctl(semid,1,SETVAL,sem); //将1号灯初始值设置为0
// eg:获取信号灯的值
val = semctl(semid,0,GETVAL); //获取0号灯的值
// eg:获取信号灯集的属性
struct semid_ds buf;
union semun sem = {
.buf = &buf,
}
semctl(semid,0,IPC_STAT,sem); //获取信号灯集的属性,第二参数被忽略
// eg:删除信号等集
semctl(semid,0,IPC_RMID); //删除信号等集

int semop(int semid, struct sembuf *sops, size_t nsops);
// 功能:信号灯集中信号灯的操作函数
// 参数:
// @semid:信号灯集的编号
// @sops:操作方式
// struct sembuf{
// unsigned short sem_num; //信号灯的编号
// short sem_op; //操作方式(PV)
// -1:P操作,申请资源
// 1:V操作,释放资源
// short sem_flg; //操作的标志位
// 0:阻塞
// IPC_NOWAIT:非阻塞方式操作
// }
// @nsops:本次操作信号灯的个数
// 返回值:成功返回0,失败返回-1置位错误码

01write.c

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
#include <head.h>
#define SHMSIZE (4096)
union semun {
int val; /* Value for SETVAL */
struct semid_ds* buf; /* Buffer for IPC_STAT, IPC_SET */
};
int mysem_init(int semid, int which, int value)
{
union semun sem = {
.val = value,
};
if (semctl(semid, which, SETVAL, sem) == -1)
PRINT_ERR("semctl error");

return 0;
}
int P(int semid, int which)
{
struct sembuf op = {
.sem_num = which, // 那个灯
.sem_op = -1, // -1 P申请 1 V释放
.sem_flg = 0, // 阻塞
};
if (semop(semid, &op, 1))
PRINT_ERR("semop P error");
return 0;
}
int V(int semid, int which)
{
struct sembuf op = {
.sem_num = which, // 那个灯
.sem_op = 1, // -1 P申请 1 V释放
.sem_flg = 0, // 阻塞
};
if (semop(semid, &op, 1))
PRINT_ERR("semop V error");
return 0;
}
int main(int argc, const char* argv[])
{
key_t key;
int shmid, semid;
char* waddr;
// 1.获取键值 ftok
if ((key = ftok("/home", 'g')) == -1)
PRINT_ERR("ftok error");
// 2.创建共享内存 shmget
if ((shmid = shmget(key, SHMSIZE, IPC_CREAT | 0666)) == -1)
PRINT_ERR("shmget error");
// 3.将共享内存映射到用户空间 shmat
if ((waddr = shmat(shmid, NULL, 0)) == (void*)-1)
PRINT_ERR("shmat error");

// 4.创建信号灯集,并初始化信号灯
if ((semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666)) == -1) {
if (errno = EEXIST) {
semid = semget(key, 2, IPC_CREAT|0666);
} else {
PRINT_ERR("semget error");
}
} else {
mysem_init(semid, 0, 1); // 将0号灯设置为1
mysem_init(semid, 1, 0); // 将1号灯设置为0
}

// 5.共享内存操作(写)
while (1) {
P(semid, 0); // 申请0号灯资源
printf("input > ");
fgets(waddr, SHMSIZE, stdin);
if (waddr[strlen(waddr) - 1] == '\n')
waddr[strlen(waddr) - 1] = '\0';
V(semid, 1); // 释放1号灯资源
if (strcmp(waddr, "quit") == 0)
break;
}
// 6.取消映射 shmdt
if (shmdt(waddr))
PRINT_ERR("shmdt error");
return 0;
}

02read.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <head.h>
#define SHMSIZE (4096)
union semun {
int val; /* Value for SETVAL */
struct semid_ds* buf; /* Buffer for IPC_STAT, IPC_SET */
};
int mysem_init(int semid, int which, int value)
{
union semun sem = {
.val = value,
};
if (semctl(semid, which, SETVAL, sem) == -1)
PRINT_ERR("semctl error");

return 0;
}
int P(int semid, int which)
{
struct sembuf op = {
.sem_num = which, // 那个灯
.sem_op = -1, // -1 P申请 1 V释放
.sem_flg = 0, // 阻塞
};
if (semop(semid, &op, 1))
PRINT_ERR("semop P error");
return 0;
}
int V(int semid, int which)
{
struct sembuf op = {
.sem_num = which, // 那个灯
.sem_op = 1, // -1 P申请 1 V释放
.sem_flg = 0, // 阻塞
};
if (semop(semid, &op, 1))
PRINT_ERR("semop V error");
return 0;
}
int main(int argc, const char* argv[])
{
key_t key;
int shmid, semid;
char* raddr;
// 1.获取键值 ftok
if ((key = ftok("/home", 'g')) == -1)
PRINT_ERR("ftok error");
// 2.创建共享内存 shmget
if ((shmid = shmget(key, SHMSIZE, IPC_CREAT | 0666)) == -1)
PRINT_ERR("shmget error");
// 3.将共享内存映射到用户空间 shmat
if ((raddr = shmat(shmid, NULL, 0)) == (void*)-1)
PRINT_ERR("shmat error");

// 4.创建信号灯集,并初始化信号灯
if ((semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666)) == -1) {
if (errno = EEXIST) {
semid = semget(key, 2, IPC_CREAT | 0666);
} else {
PRINT_ERR("semget error");
}
} else {
mysem_init(semid, 0, 1); // 将0号灯设置为1
mysem_init(semid, 1, 0); // 将1号灯设置为0
}

// 5.从共享内存中数数据
while (1) {
P(semid, 1); // 申请1号灯资源
printf("read:%s\n", raddr);
V(semid, 0); // 释放0号灯资源
if (strncmp(raddr, "quit", 4) == 0)
break;
}
// 6.取消映射 shmdt
if (shmdt(raddr))
PRINT_ERR("shmdt error");
// 7.删除共享内存 shmctl
if (shmctl(shmid, IPC_RMID, NULL))
PRINT_ERR("shmctl error");
// 8.删除信号灯集
if (semctl(semid, 0, IPC_RMID))
PRINT_ERR("semctl error");
return 0;
}

套接字

socket 套接字本来是用于同主机的进程间通信的。后来有了TCP/IP协议族加入后又进行的功能的拓展与升级,实现了多个不同主机间的进程间通信。
socket 是一个系统调用的接口,会返回一个文件描述符,用户通过网络收发数据时,只需对此文件描述符进行读写操作即可,读就是接收,写就是发送。相当于把复杂的网络通信过程转换成了IO操作。

此处之涉及本地间的通信,不涉及网络通信,更多在另一篇文章中进行介绍。

本地间 TCP 通信

服务器

  • 创建套接字 socket
  • 填充服务器本地信息结构体 sockaddr_un
  • 将套接字与服务器本地信息结构体绑定 bind
  • 将套接字设置为被动监听状态 listen
  • 阻塞等待客户端的连接请求 accept
  • 进行通信 recv / sendread / write

客户端

  • 创建套接字 socket
  • 填充服务器本地信息结构体 sockaddr_un
  • 发送客户端的连接请求 connect
  • 进行通信 send / recv

本地通信结构体

1
man 7 unix # 可以查看到结构体的定义
1
2
3
4
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[108]; // pathname
};

客户端

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 <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/un.h>

int main(int argc, char const *argv[])
{
//1.创建流式套接字:
int socket_fd = socket(AF_LOCAL,SOCK_STREAM,0);
if(socket_fd == -1)
{
perror("socket err:");
return -1;
}
//2.创建一个网络信息结构体:
struct sockaddr_un serverInfo = {0};
serverInfo.sun_family = AF_LOCAL;
const char* processfileName = "myserver";
memcpy(serverInfo.sun_path,processfileName,strlen(processfileName));

//使用connect函数连接主机:
int ret = connect(socket_fd,(const struct sockaddr*)&serverInfo,sizeof(serverInfo));
if(ret == -1)
{
perror("connect err:");
return -1;
}
char buf[128] = {0};
while(true)
{
memset(buf,0,sizeof(buf));
//发出数据:
fgets(buf,sizeof(buf),stdin);
int nbytes = write(socket_fd,buf,strlen(buf));
if(nbytes == -1)
{
perror("write err:");
return -1;
}
//接收数据:
memset(buf,0,sizeof(buf));
nbytes = read(socket_fd,buf,sizeof(buf));
if(nbytes == -1)
{
perror("read err:");
return -1;
}
if(nbytes == 0)
{
printf("对端已经关闭了\n");
return 0;
}
printf("服务器发来的数据: %s \n",buf);
}
return 0;
}

服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/un.h>

int main(int argc, char const *argv[])
{
// 1.创建流式监听套接字类型:本地套接字。
int listen_fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (listen_fd == -1)
{
perror("socket() err:");
return -1;
}
// 2.定义一个网络地址信息结构体:
struct sockaddr_un serverInfo;
memset(&serverInfo, 0, sizeof(serverInfo));
serverInfo.sun_family = AF_LOCAL;
const char* processfileName = "myserver";
memcpy(serverInfo.sun_path,processfileName,strlen(processfileName));
// 端口号都是2字节,short类型,所以一定要注意字节序的问题:
// 3.绑定网络地址信息结构体:
int ret = bind(listen_fd, (const struct sockaddr *)&serverInfo, sizeof(serverInfo));
if (ret == -1)
{
perror("bind err:");
return -1;
}
// 4.设置监听的状态:
ret = listen(listen_fd, 1);
if (ret == -1)
{
perror("listen err:");
return -1;
}
// struct sockaddr clientInfo = {0};//如果关于客户端地址信息就写一个客户端的信息结构体。
// int clientlen;
// 5.阻塞等待客户端的连接:
printf("本地套接字通信服务进程启动\n");
while (true)
{
int connect_fd = accept(listen_fd, NULL, NULL);
if (connect_fd == -1)
{
perror("accept err:");
return -1;
}
while (true)
{
// 数据的收发:
// 定义用户数据的缓冲区buf;
char buf[128] = {0};
int nbtyes = read(connect_fd, buf,sizeof(buf));
if (nbtyes == -1)
{
perror("read err:");
return -1;
}
if(nbtyes == 0)
{
printf("对端断开了连接\n");
close(connect_fd);
break;
}
printf("客户端发来的数据为 %s \n",buf);
}
}

return 0;
}

本地间 UDP 通信

服务器:

  • 创建套接字 socket
  • 填充服务器本地信息结构体 sockaddr_un
  • 将套接字与服务器本地信息结构体绑定 bind
  • 进行通信 recvfrom / sendto

客户端:

  • 创建套接字 socket
  • 填充客户端本地信息结构体 sockaddr_un
  • 将套接字与客户端本地信息结构体绑定 bind如果不绑定 服务器没法给客户端回信
  • 填充服务器本地信息结构体 sockaddr_un
  • 进行通信 sendto/ recvfrom

服务器

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
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <stdlib.h>
#include <sys/un.h>
int main(int argc, char const *argv[])
{
//1.创建用户数据报类套接字本地通信域:
int socket_dgram = socket(AF_LOCAL,SOCK_DGRAM,0);
if(socket_dgram == -1)
{
perror("socket err:");
return -1;
}

//bind网络信息结构体:
struct sockaddr_un serverInfo = {0};
serverInfo.sun_family = AF_LOCAL;
//构建本地socket套接字文件:
const char* processfileName = "Local_udp_server";
memcpy(serverInfo.sun_path,processfileName,strlen(processfileName));

int ret = bind(socket_dgram,(struct sockaddr*)&serverInfo,sizeof(serverInfo));
if(ret == -1)
{
perror("bind err:");
return -1;
}
//接收数据与发送数据:
//定义一个接收方的信息结构体:
struct sockaddr_un udp_clientInfo = {0};
int info_len = sizeof(udp_clientInfo);
//定义一个用户数据的缓冲buf;
char buf[128] = {0};
printf("本地udp进程服务启动\n");
while(true)
{
memset(buf,0,sizeof(buf));
int nbytes = recvfrom(socket_dgram,buf,sizeof(buf),0,
(struct sockaddr*)&udp_clientInfo,&info_len);
if(nbytes == -1)
{
perror("recvfrom err:");
return -1;
}
if(nbytes == 0)
{
printf("没有接收到任何数据\n");
continue;
}
printf("udp客户进程文件:[%s] 发来数据->\n",
udp_clientInfo.sun_path);
printf("udp服务器接收的数据为: %s \n",buf);
//udp 回显:
nbytes = sendto(socket_dgram,buf,strlen(buf),0,
(struct sockaddr*)&udp_clientInfo,sizeof(udp_clientInfo));
if(nbytes == -1)
{
perror("sendto err:");
return -1;
}
}
return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/un.h>

int main(int argc, char const *argv[])
{
//构建UDP服务器:
//1.构建用户数据报套接字:
int udp_fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
if (udp_fd == -1)
{
perror("socket err:");
return -1;
}

//2.构建网络地址信息结构体:
struct sockaddr_un udp_serverInfo = {0};
udp_serverInfo.sun_family = AF_LOCAL;
//改成socket文件的形式:
//构建本地socket套接字文件:
const char* processfileName = "Local_udp_server";
//strncpy(udp_serverInfo.sun_path, processfileName, sizeof(udp_serverInfo.sun_path) - 1);
memcpy(udp_serverInfo.sun_path, processfileName,strlen(processfileName));
//绑定本地的客户端的套接字文件:
struct sockaddr_un udp_clientInfo = {0};
udp_clientInfo.sun_family = AF_LOCAL;
//改成socket文件的形式:
//构建本地socket套接字文件:
const char* clientfileName = "Local_udp_client";
//strncpy(udp_clientInfo.sun_path, clientfileName, sizeof(udp_clientInfo.sun_path) - 1);
memcpy(udp_clientInfo.sun_path, clientfileName,strlen(clientfileName));
socklen_t len = sizeof(udp_clientInfo);
int ret = bind(udp_fd, (struct sockaddr*)&udp_clientInfo, len);

//3.定义一用户数据缓冲区:
char buf[128] = {0};
while (true)
{
memset(buf, 0, sizeof(buf));
printf("请输入你要发送的udp数据:\n");
fgets(buf, sizeof(buf), stdin);
//发送数据:sendto:
int nbytes = sendto(udp_fd, buf, strlen(buf), 0, (struct sockaddr*)&udp_serverInfo, sizeof(udp_serverInfo));
if (nbytes == -1)
{
perror("sendto err:");
return -1;
}
memset(buf, 0, sizeof(buf));
//接收数据:recvfrom:
nbytes = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&udp_clientInfo, &len);
if (nbytes == -1)
{
perror("recvfrom err:");
return -1;
}
printf("对端发来的数据为:%s \n", buf);
}
return 0;
}

守护进程

守护进程是后台运行的进程,它会随着系统的启动而启动,会随着系统的终止而终止,类似于 windows 上的各种服务。
例如 windows 上的网络管理的服务,ubuntu 上的 sshd 的服务,ubuntu 上的 资源管理 的进程

守护进程创建的流程

  1. 创建孤儿进程

  2. 设置孤儿进程的会话id和组id

    1
    2
    3
    4
    5
    pid_t setsid(void);
    // 功能:设置会话id
    // 参数:
    // @无
    // 返回值:成功返回新的会话id,失败返回-1,置位错误码
  3. 切换目录到/var/log目录下

    1
    2
    3
    4
    5
    6

    int chdir(const char *path);
    // 功能:切换目录
    // 参数:
    // @path:路径
    // 返回值:成功返回0,失败返回-1置位错误码
  4. 设置umask的值0

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

    mode_t umask(mode_t mask);
    // 功能:设置文件的掩码
    // 参数:
    // @mask:掩码值
    // 返回值:返回设置前的掩码值,总是会成功
  5. 创建日志文件

    1
    2
    3
    int fd;
    if((fd = open("./daemon.log",O_RDWR|O_CREAT|O_APPEND,0666))==-1)
    PRINT_ERR("open error");
  6. 文件描述符重定向工作

    1
    2
    3
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
  7. 开启自己的服务

文件描述符重定向

dup dup2 函数

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

int dup(int oldfd);
// 功能:将使用oldfd生成newfd,newfd采用文件描述符最小未使用的原则分配的。
// oldfd和newfd都能操作文件,两者共用光标。
// 参数:
// @oldfd:旧的文件描述符
// 返回值:成功返回新的文件描述符,失败返回-1置位错误码


int dup2(int oldfd, int newfd);
// 功能:将oldfd重定向到newfd中,newfd是用户自己指定的,如果newfd
// 之前被分配过在使用前会关闭它们。
// 参数:
// @oldfd:旧的文件描述符
// @newfd:新的文件描述符
// 返回值:成功返回新的文件描述符,失败返回-1置位错误码

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
#include <head.h>

int main(int argc, const char* argv[])
{
int fd;
if ((fd = open("./daemon.log", O_RDWR | O_CREAT | O_APPEND, 0666)) == -1)
PRINT_ERR("open error");

//关闭标准输入,标准输出,标准出错
close(0);
close(1);
close(2);

//将0,1,2重定向到文件fd中
dup(fd);
dup(fd);
dup(fd);

printf("hello daemon dup process...\n");
fflush(stdout);
fprintf(stderr,"this is test daemon dup stderr...\n");
lseek(fd,0,SEEK_SET);
char ch;
scanf("%c",&ch);
printf("ch = %c\n",ch);
return 0;
}

dup2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <head.h>

int main(int argc, const char* argv[])
{
int fd;
if ((fd = open("./daemon.log", O_RDWR | O_CREAT | O_APPEND, 0666)) == -1)
PRINT_ERR("open error");

dup2(fd,0);
dup2(fd,1);
dup2(fd,2);

printf("hello daemon dup process...\n");
fflush(stdout);
fprintf(stderr,"this is test daemon dup stderr...\n");
lseek(fd,0,SEEK_SET);
char ch;
scanf("%c",&ch);
printf("ch = %c\n",ch);
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
#include <head.h>

int main(int argc, const char* argv[])
{
pid_t pid;
int fd;
if ((pid = fork()) == -1) {
PRINT_ERR("fork error");
} else if (pid == 0) {
// 1.孤儿进程
// 2.设置会话id
if(setsid()==-1)
PRINT_ERR("setsid error");
// 3.切换目录(/var/log/)
if (chdir("/"))
PRINT_ERR("chdir error");
// 4.修改掩码
umask(0);
// 5.创建日志文件
if ((fd = open("./daemon.log", O_RDWR | O_CREAT | O_APPEND, 0666)) == -1)
PRINT_ERR("open error");
// 6.文件描述符重定向
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
// 7.开启自己的服务
char s[] = "i am test daemon process\n";
while(1){
write(1,s,strlen(s));
sleep(1);
}

} else {
// 让父进程退出
exit(0);
}
return 0;
}

进程内程序替换函数

终端执行 a.out 程序, 首先会执行fork产生一个子进程,当产生子进程之后,子进程的 .text段存放的是终端的可执行程序,需要将这个段内的内容替换成 a.out 。此时就可以执行 a.out 应用程序了,以上的功能可以通过 system 完成。

system 函数

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

int system(const char *command);
// 功能:首先会fork一个子进程,在子进程内执行command这个可执行程序
// 参数:
// @command:可执行程序的路径及名字
// 返回值:
// *如果command为NULL,如果终端可用返回非0,如果终端不可用返回0
// *如果无法创建子进程,或者无法检索其状态,则返回值为-1。
// *如果命令不能在子进程中执行,返回退出状态(效果和exit(status)一样的)
// *如果所有系统调用都成功,则返回值是用于执行命令的子shell的终止状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <head.h>

int main(int argc, const char* argv[])
{
// 首先会创建一个子进程,在子进程内执行"/bin/ls"可执行程序
// if(system("/bin/ls -l"))
// PRINT_ERR("system error");

// if(system("./b.out"))
// PRINT_ERR("system error");

if (system("./myshell.sh 111 222 333 4444 555"))
PRINT_ERR("system error");

return 0;
}

函数接口

fopen fclose fgetc fputc fgets fputs fread fwrite fseek ftell rewind
perror strerror snprintf sprintf fprintf time localtime
open read write close lseek stat lstat getpwuid getgrgid
opendir readdir closedir
fork getpid getppid exit _exit wait waitpid dup dup2 system
pthread_create pthread_self pthread_exit pthread_join pthread_detach pthread_cancel
pthread_mutex_init pthread_mutex_lock pthread_mutex_unlock pthread_mutex_destroy
sem_init sem_wait sem_post sem_destroy
pthread_cond_init pthread_cond_wait pthread_cond_signal
pthread_cond_broatcast pthread_cond_destroy
pipe mkfifo signal kill raise alarm
ftok msgget msgsnd msgrcv msgctl
shmget shmat shmdt shmctl
semget semctl semop