10.09
注意:这个是Linux高级编程的简明教程,是Linux应用程序的开发,而不是底层程序的开发。

内容是关于操作系统网络编程的吗?

Linux简明系统编程

  • 〇、课程思维导图
  • 〇、会用到的头文件
  • 〇、视频课+参考笔记
  • 一、任务、程序、进程、线程概念和区别
    • 第1节课:程序进程线程概念、进程ID号
      • 1.程序、进程、线程的概念
      • 2.进程号pid
      • 3.查看进程号的两个函数:getpid() 和 getppid()
      • 4.显示进程树:pstree -p
        • systemd(1):进程号为1,它是所有进程的父进程
  • 二、进程及多进程编程
    • 第2、3节课:进程创建函数fork()
      • fork函数之后父子进程谁先执行?
      • 子进程和父进程之间是相互独立的,互不干扰
    • 第4节课:监控子进程函数wait()
  • 三、线程及多线程编程
    • 第5节课:创建线程函数pthread_create()
    • 第6节课:多线程及线程间数据共享
  • 四、任务间通信与同步(7种方式)
    • ☆☆☆①任务间的通信 之 管道pipe
    • 第7、8、9、10节课:无名管道、测试无名管道大小、练习、两条管道双向传输
      • 7.无名管道
      • 8.如何测试无名管道大小?
      • 9.练习:子进程通过键盘输入内容并写入管道,父进程读取管道里的内容并打印
      • 10.两条管道双向传输
    • 第11节课:有名管道(命名管道)---mkfifo函数
    • ☆☆☆②任务间的通信 之 共享内存 shared memory
    • 第12节课:共享内存 shared memory
    • 第13节课:非亲缘关系进程通过共享内存通信
    • ☆☆☆③任务间的通信 之 消息队列message queue
    • 第14节课:消息队列message queue
    • 第15节课:非亲缘关系进程通过消息队列通信
      • 补充:创建键值key的函数:ftok()函数
    • ☆☆☆④任务间的同步 之 信号量semaphore
    • 第16节课:无名信号量
    • 第17节课:命名信号量
    • 第18节课:线程间信号量同步
    • ☆☆☆⑤任务间的同步 之 互斥锁mutex
    • 第19节课:互斥锁(常用在线程间的同步)
    • ☆☆☆⑥内核和应用进程间/进程和进程间 传递的控制命令 之 信号signal
    • 第20节课:信号signal
    • ☆☆☆⑦socket套接字
  • 五、Linux网络编程

〇、课程思维导图

〇、会用到的头文件

#include<sys/types.h>//fork函数、wait函数、mkfifo函数(有名管道)、msgget函数(消息队列)、kill函数(发送信号signal)
#include<unistd.h>//fork函数、pipe函数
#include<sys/stat.h>//mkfifo函数(有名管道)、sem_open函数
#include<sys/wait.h>//wait函数、waitpid函数
#include<pthread.h>//pthread_create函数(创建线程)、几个互斥锁函数pthread_mutex_init函数、pthread_mutex_lock函数、pthread_mutex_unlock函数
#include<fcntl.h>//read、open、write、close文件、sem_open函数
#include<sys/ipc.h>//shmget函数
#include<sys/shm.h>//shmget函数
#include<sys/ipc.h>//msgget函数
#include<sys/msg.h>//msgget函数
#include<semaphore.h>//sem_init函数、sem_wait函数、sem_post函数、sem_open函数、sem_close函数
#include<sys/mman.h>//mmap函数
#include<signal.h>//kill函数(发送信号signal)、signal函数()
#include<iostream>
using namespace std;int main(){...return 0;
}

〇、视频课+参考笔记

视频课就来自于嵌入式技术公开课公众号的老师讲的一个课程,名称就叫Linux简明系统编程
这个CSDN专栏写的很好:linux系统编程。

一、任务、程序、进程、线程概念和区别

第1节课:程序进程线程概念、进程ID号

1.程序、进程、线程的概念

程序:

  • 源代码,指令,程序是静态的概念,比如一个安装包,就存放在电脑磁盘里,不进行任何操作

进程:

  • 正在执行的程序的实例,是程序的动态的概念,比如qq和微信是两个独立的进程。进程的创建和销毁会带来很大的资源消耗;每个进程都有独立的进程号(相当于一个编号,套接字Socket=(IP地址:端口号))
    进程之间是相互独立的。

线程:

  • 线程从属于进程,一个进程可以有多个线程线程之间共享进程的资源

任务:

  • 具体要做的事情。

2.进程号pid

每个进程都有一个进程号,叫pid(process id),

man getpid //查询手册

3.查看进程号的两个函数:getpid() 和 getppid()

两个函数的参数都是空,返回值为pid_t类型。

4.显示进程树:pstree -p

-p表示pid,即进程号。

pstree -p

systemd(1):进程号为1,它是所有进程的父进程

systemd(1):进程号为1,它是所有进程的父进程

二、进程及多进程编程

第2、3节课:进程创建函数fork()

fork()函数:返回值依然是pid_t类型。

man fork


通过复制当前的进程,创建一个新进程,新的进程是当前进程的子进程。

示例:

#include<sys/types.h>
#include<unistd.h>
#include<iostream>
using namespace std;int main(){pid_t pid;pid = fork();cout << "pid = " << pid << endl;cout << "hello, world" << endl;return 0;
}

结果:

//父进程返回的内容:
pid = 1200482
hello, world
//子进程返回的内容:
pid = 0
hello, world

解释:

父进程返回的是子进程的PID,子进程返回0;
所以说上面的1200482是新创建的子进程的PID,这个信息是由父进程来返回的。

程序修改一下:

#include<sys/types.h>
#include<unistd.h>
#include<iostream>
using namespace std;int main(){// pid_t pid;// pid = fork();// cout << "pid = " << pid << endl;// cout << "hello, world" << endl;pid_t pid1, pid2;pid1 = fork();pid2 = fork();cout << "pid1 = " << pid1 << "; pid2 = " << pid2 << endl;return 0;
}

结果会是什么呢?

pid1 = 0; pid2 = 0 //进程D(拷贝进程B,所以和B的pid1相同)
pid1 = 0; pid2 = 1204505 //进程B
pid1 = 1204503; pid2 = 1204504 //进程A
pid1 = 1204503; pid2 = 0 //进程C(拷贝进程A,所以和A的pid1相同)

进程A的pid未知;进程B的pid为1204503;
进程C的pid为1204504;进程D的pid为1204505;
解释:

以下内容参考链接:https://www.csdn.net/tags/OtDakg2sMTU0OTItYmxvZwO0O0OO0O0O.html
程序1:

#include<sys/types.h>
#include<unistd.h>
#include<iostream>
using namespace std;int main(){pid_t pid;pid = getpid();fork();cout << "pid = " << pid << "; getpid() = " << getpid() << endl;//pid为父进程的进程ID号,而getpid()是获取当前进程的ID号。return 0;
}

结果:

pid = 1336259; getpid() = 1336259 //父进程
pid = 1336259; getpid() = 1336264 //子进程

由于fork函数创建了一个新的子进程,所以fork函数后的语句会执行两次,也就是打印两次进程的pid号。因为cout语句第一次运行在父进程,所以两个pid的值相等,但cout语句第二次运行在子进程,所以两个pid就不相等了,也就是说此时的pid2是子进程的进程ID。

程序2:

#include<sys/types.h>
#include<unistd.h>
#include<iostream>
using namespace std;int main(){pid_t pid;pid = getpid();cout << "before fork, pid = " << getpid() << endl;fork();cout << "after fork, pid = " << getpid() << endl;//cout << "pid = " << pid << "; getpid() = " << getpid() << endl;if(getpid() == pid) cout << "这是父进程,此时的pid = " << getpid() << endl;else cout << "这是子进程,此时的pid = " << getpid() << endl;return 0;
}

结果:


before fork, pid = 1356246
after fork, pid = 1356251
这是子进程,此时的pid = 1356251
after fork, pid = 1356246
这是父进程,此时的pid = 1356246

程序3:fork函数的返回值

#include<sys/types.h>
#include<unistd.h>
#include<iostream>
using namespace std;int main(){pid_t pid;//pid = getpid();cout << "before fork, pid = " << getpid() << endl;pid = fork();//cout << "after fork, pid = " << getpid() << endl;//cout << "pid = " << pid << "; getpid() = " << getpid() << endl;if(pid == 0) cout << "这是子进程,此时的pid = " << getpid() << endl;else cout << "这是父进程,此时的pid = " << getpid() << endl;return 0;
}

结果:

before fork, pid = 1356723
这是子进程,此时的pid = 1356728
这是父进程,此时的pid = 1356723

参考链接2:
这篇讲的也很好,可以直接看这篇博客 linux中fork函数及子进程父进程执行顺序

参考链接3:
操作系统fork()进程

    pid_t pid;pid = fork();cout << "pid = " << pid << endl;

fork是返回两个值:
一个代表父进程:代表父进程的值是一串数字,这串数字是子进程的ID(地址);
一个代表子进程:返回值为0。

多次手动运行这个程序,会发现以下两个结果:

//第一种结果:
pid = 1358110
pid = 0//第二种结果:
pid = 0
pid = 1358828

说明fork函数之后先运行父进程还是子进程,是不确定的,父子进程在争用系统资源,看谁先执行。

参考链接4:
fork() && fork() || fork();会产生几个子进程
关键在下面的理解:

fork函数之后父子进程谁先执行?

上面的参考链接3中的程序:
注意:不能通过判断谁先打印就说谁先执行,因为打印函数本来就是个很复杂的过程,并不能说先打印出父进程的pid就说先执行的父进程

宏观上来说是同时执行;
微观上来说是交替进行的;
这就是并发;计算机操作系统笔记—并行和并发的区别

Linux2.6之后默认是父进程先调用,因为父进程一直处于活跃状态;并且父子进程谁先执行带来的影响几乎可以忽略不计,如果一定要先让谁运行,后让谁运行,这就涉及到后面要说的同步问题,同步就是让程序按照人为设定的顺序去执行

问:执行一次fork函数 先运行子进程还是先运行父进程?
答:(闫波)
没有顺序的
为了可以同步(同步就是有顺序),所以引入了信号量之类的东西;
执行同步就是让程序以一个确认的顺序运行,其实自己项目多线程用的多,包括咱们科研的,进程环境切换代价太大了,一般都是多线程
现在电脑也是,8核16线程这种,线程用的多,高并发也是线程
参考链接:Linux C++多线程同步的四种方式

子进程和父进程之间是相互独立的,互不干扰

让子进程和父进程都执行一个for循环,子进程执行20次,父进程执行10次

#include<sys/types.h>
#include<unistd.h>
#include<iostream>
using namespace std;int main(){pid_t pid;int count = 0;pid = fork();if(pid == 0){for(int i = 0; i < 20; ++i){//while(1)++count;cout << "子进程:执行次数" << count << endl;sleep(2);}}else if(pid > 0){for(int i = 0; i < 10; ++i){//while(1)++count;cout << "父进程:执行次数" << count << endl;sleep(2);}  }elseperror("fork");return 0;
}

结果是父子进程各自值行自己的++countcout操作,所以父子进程之间是相互独立的,互不影响;
另外当父进程执行10次之后,子进程会继续执行,直到打印完20次才停止,也就是说父进程的结束并不会导致子进程也停止

注意,不要写上面的那个while(1),那样会导致子进程根本停不下来,Ctrl + C也没用。

第4节课:监控子进程函数wait()

man 2 wait


所有这些系统调用都用于等待调用进程的子进程的状态更改并获取状态已更改的子进程的信息
wait()系统调用暂停调用线程的执行,直到它的一个子线程终止。
waitpid()系统调用暂停调用线程的执行,直到pid参数指定的子线程改变状态。默认情况下,waitpid()只等待终止的子进程。
返回值:
wait():如果成功,返回被终止子进程的pid;发生错误时,返回-1
waitpid():如果成功,返回状态改变的子进程的pid;如果指定了WNOHANG,并且pid指定的一个或多个子进程(ren)已经存在,但是还没有改变状态,则返回0。发生错误时,返回-1。

示例:
创建三个子进程,分别在5秒、10秒、15秒之后结束,父进程一直等待,直到所有子进程都运行结束,父进程才停止运行,在这个过程中,父进程一直监控几个子进程的运行状态。

代码:

#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<iostream>
using namespace std;int main(){int arr[4] = {0, 10, 5, 15};pid_t child_pid;for(int i = 1; i <= 3; ++i){switch (fork()){case -1:perror("fork");exit(0);//break;case 0:cout << "子进程 " << i << " 已创建,pid = " << getpid() << ",ppid = " << getppid() << ",sleeping 时长为 " << arr[i] << "秒。" << endl;sleep(arr[i]);exit(0);//break;default:break;}}int numDead = 0;while(true){child_pid = wait(nullptr);//等待子进程结束if(child_pid < 0){cout << "子进程都结束了,再见!" << endl;break;}++numDead;cout << "wait()函数返回了 pid = " << child_pid << "的子进程,它是第 " << numDead << " 个结束的子进程;" << endl;}return 0;
}

结果:

子进程 1 已创建,pid = 4168680,ppid = 4168679,sleeping 时长为 10秒。
子进程 2 已创建,pid = 4168681,ppid = 4168679,sleeping 时长为 5秒。
子进程 3 已创建,pid = 4168682,ppid = 4168679,sleeping 时长为 15秒。
wait()函数返回了 pid = 4168681的子进程,它是第 1 个结束的子进程;
wait()函数返回了 pid = 4168680的子进程,它是第 2 个结束的子进程;
wait()函数返回了 pid = 4168682的子进程,它是第 3 个结束的子进程;
子进程都结束了,再见!

三、线程及多线程编程

第5节课:创建线程函数pthread_create()

程序:源代码,指令,程序是静态的概念,比如一个安装包,就存放在电脑磁盘里,不进行任何操作

进程:正在执行的程序的实例,是程序的动态的概念,比如qq和微信是两个独立的进程。
1.进程的创建和销毁会带来很大的资源消耗
2.每个进程都有独立的进程号PID
3.注意:进程之间是相互独立的
4.注意:套接字中的端口号进程号不是一回事;
套接字Socket = (IP地址:端口号)
端口号和进程号的关系:

线程:线程从属于进程,一个进程可以有多个线程,线程之间共享进程的资源

max pthread_create

头文件:

#include<pthread.h>

四个参数:

pthread_t *threak
//pthread_t* 类型,表示线程ID号,这里写某个pthread_t类型变量的地址
const pthread_attr_t *attr,
//线程结构体指针类型,一般写null
void *(*start routine)(void *)
//函数指针类型,函数是指某个线程函数,函数的参数是void*,返回值也是void*,这里写函数的地址,即函数名
void *arg
//传递给线程函数的参数,一般写null

返回值:int类型,如果成功创建了一个线程,就返回0;否则返回一个错误的数字。

示例:

#include<sys/types.h>
#include<pthread.h>
#include<unistd.h>
#include<iostream>
using namespace std;//线程函数:
void* thread_func(void* arg){return nullptr;
}
int main(){pthread_t pthread;//线程ID号int res;res = pthread_create(&pthread, nullptr, thread_func, nullptr);cout << "线程创建结果:" << res << endl;if(res != 0){cout << "线程创建失败!!!" << endl;perror("pthread_create");exit(1);}elsecout << "线程创建成功!" << endl;//注意:这里是创建完线程之后,就return 0了,意味着创建完线程进程就结束了return 0;
}

调试结果:

接下来给线程函数中添加内容,让线程函数执行一些任务(每隔1秒打印一个hello world)。
但有个问题,上面的main函数中,创建完线程之后,就return 0了,意味着创建完线程整个程序就结束了,那么进程也就结束了,而创建的线程是共享进程资源的,所以此时线程函数中的内容并没有执行,因此我们就需要进程等待线程执行完之后再结束,就要用到一个函数pthread_join()

man pthread_join


两个参数:
pthread_t thread //线程号,表示等哪个线程结束
第二个参数一般写null

返回值:int类型,函数调用成功,返回0,否则返回错误的数字。

使用pthread_create函数开始分叉,函数的第一个参数就是线程的标号,第二个参数暂时用不到,写null,第三个参数是在该线程执行的函数(线程函数),函数的返回值和参数都是void*,第四个是参数传递函数,也写null;
运行线程的过程就是运行4个hello函数的过程;
使用pthread_join函数将各个线程合并,结束线程,函数的第一个参数是线程的标号,第二个参数暂时不用,写null

示例1:(创建一个线程

#include<sys/types.h>
#include<pthread.h>
#include<unistd.h>
#include<iostream>
using namespace std;//线程函数:打印6次hello, world
void* thread_func(void* arg){for(int i = 0; i < 6; ++i){//sleep(2);cout << "hello, world. 第" << i + 1 << "遍" << endl;sleep(2);}return nullptr;
}
int main(){pthread_t pthread;//线程ID号int res;res = pthread_create(&pthread, nullptr, thread_func, nullptr);//cout << "线程创建结果:" << res << endl;if(res != 0){cout << "线程创建失败!!!" << endl;perror("pthread_create");exit(1);}elsecout << "线程创建成功!线程创建成功!" << endl;//线程创建成功!线程创建成功!线程创建成功!线程创建成功!线程创建成功!//线程合并,也即结束线程pthread_join(pthread, nullptr);cout << "线程结束了!" << endl;return 0;
}

编译运行:

第6节课:多线程及线程间数据共享

示例2:(创建多个线程

#include<sys/types.h>
#include<pthread.h>
#include<unistd.h>
#include<iostream>
using namespace std;int count = 100;
//线程函数:打印6次hello, world
void* thread1_func(void* arg){for(int i = 0; i < count; ++i){//sleep(2);//cout << "hello, world. 第" << i + 1 << "遍" << endl;cout << "线程1" << endl;sleep(1);}return nullptr;
}
//线程函数:打印6次good, morning
void* thread2_func(void* arg){for(int i = 0; i < count; ++i){//sleep(2);//cout << "good, morning. 第" << i + 1 << "遍" << endl;cout << "线程2" << endl;sleep(1);}return nullptr;
}
int main(){pthread_t pthread1, pthread2;//线程ID号int res;
//线程1的创建:res = pthread_create(&pthread1, nullptr, thread1_func, nullptr);//cout << "线程创建结果:" << res << endl;if(res != 0){cout << "线程1创建失败!!!" << endl;perror("pthread_create");exit(1);}elsecout << "线程1创建成功!线程1创建成功!" << endl;//线程创建成功!线程创建成功!线程创建成功!线程创建成功!线程创建成功!
//线程2的创建:res = pthread_create(&pthread2, nullptr, thread2_func, nullptr);//cout << "线程创建结果:" << res << endl;if(res != 0){cout << "线程2创建失败!!!" << endl;perror("pthread_create");exit(1);}elsecout << "线程2创建成功!线程2创建成功!" << endl;//线程创建成功!线程创建成功!线程创建成功!线程创建成功!线程创建成功!
//线程合并:pthread_join(pthread1, nullptr);pthread_join(pthread2, nullptr);cout << "线程结束了!" << endl;return 0;
}

编译运行:

注意:
两个线程之间是并发的关系,它们抢占系统资源,谁抢到就先执行谁;
两个线程之间是共享进程的资源的。

四、任务间通信与同步(7种方式)

任务间通信与同步的方式:管道、信号、信号量、互斥锁、消息队列、共享内存、socket套接字
进程之间的通信要难一些,因为进程之间是相互独立的;
线程之间通信就要相对简单一些了。

☆☆☆①任务间的通信 之 管道pipe

第7、8、9、10节课:无名管道、测试无名管道大小、练习、两条管道双向传输

7.无名管道

管道pipe分为两类:

  • 有名管道(命名管道):named pipe;
  • 无名管道(未命名管道):unnamed pipe;

man pipe

可通过pipe函数可实现进程间的通信单向数据通道unidirectional data channel
函数的参数是个数组,数组中的首元素表示管道的读端,第二个元素表示管道的写端
返回值:如果函数调用成功,就返回0,否则返回-1;

如何用pipe函数实现进程间的通信?
两个进程:父进程和子进程;
父进程的写端打开,读端关闭;
子进程的读端打开,写端关闭;
管道是单向的,只能从一端到另一端;

具体实现时,fork函数写在pipe函数后面,这样子进程也会复制父进程的管道,只需把父进程的写端打开、把子进程的读端打开即可。

fd表示文件描述符,file description。

示例:

#include<unistd.h>
#include<sys/types.h>
#include<string>
#include<iostream>
using namespace std;
//父进程:写管道
//子进程:读管道
int main(){int fd[2];if(pipe(fd) == -1){perror("pipe");}pthread_t pid;pid = fork();char str[6];if(pid > 0){//父进程close(fd[0]);//关闭读端sleep(5);//过5秒之后才write数据给父进程管道的写端write(fd[1], "123456", 6);//往管道的写端写数据,每次写6个字符:"123456"}else if(pid == 0){//子进程cout << "子进程正在等待数据。。。" << endl;close(fd[1]);//关闭写端read(fd[0], str, 6);//从管道的读端读取数据,每次读6个字符cout << "子进程读到的数据为:" << str << endl;}return 0;
}

编译运行:

8.如何测试无名管道大小?

思路:
子进程写数据到管道中,并且统计写了多少count个字节的数据;
父进程啥也不干,就是等待,直到子进程结束(调用waitpid函数);
子进程的写操作放在一个死循环while(1)中,当把管道写满时,进程依然不会结束,但此时就写不进去了,只是为了看此时的count值是多少,就可以测试出无名管道的大小。

代码:

#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<iostream>
using namespace std;int main(){pid_t pid;int fd[2];if(pipe(fd) == -1)perror("pipe");int count = 0;pid = fork();if(pid == 0){//子进程char ch = '*';close(fd[0]);//关闭读端while(1){write(fd[1], &ch, 1);//这里ch前要加地址符 往管道的写端写数据,每次写一个字符*cout << "count = " << ++count << endl;//记录往管道中写入了多少个字节}}else if(pid > 0){//父进程waitpid(pid, NULL, 0);//一直等子进程运行结束}return 0;
}

编译运行:


最终结果是65536个字节,所以说无名管道的大小是65536个字节

9.练习:子进程通过键盘输入内容并写入管道,父进程读取管道里的内容并打印

让子进程通过键盘输入内容,并把内容写入管道的写端;
让父进程读取管道的读端的内容,并打印出来。

代码:

#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include<iostream>
using namespace std;int main(){pid_t pid;int fd[2];if(pipe(fd) == -1)perror("pipe");pid = fork();if(pid == 0){//子进程:从键盘输入内容给到管道的写端char info[100];close(fd[0]);//关闭读端while(1){cin.getline(info, 100);//从键盘读取输入的内容write(fd[1], info, sizeof(info));}}else if(pid > 0){//父进程:从管道的读端读取子进程通过键盘输入的内容char tmp[100];close(fd[1]);//关闭写端while(1){cout << "父进程正在等待子进程输到管道里的内容。。。" << endl;read(fd[0], tmp, sizeof(tmp));cout << "从子进程读取的内容为:" << tmp << endl;}}return 0;
}

编译运行:

10.两条管道双向传输

子进程:从管道1的读端读取子进程通过键盘输入的内容,然后将大小写转换之后再写到管道2的写端;
父进程:从键盘输入内容给到管道1的写端,然后从管道2的读端读取变换大小写之后的内容。

代码:

#include<unistd.h>
#include<iostream>
#include<ctype.h>
using namespace std;
int main(){//两个管道:int fd1[2];int fd2[2];int res = pipe(fd1);if(res == -1) perror("pipe");res = pipe(fd2);if(res == -1) perror("pipe");pid_t pid = fork();if(pid > 0){//父进程:通过键盘给管道1中写数据,用管道2来接收数据close(fd1[0]);//关闭管道1的读端close(fd2[1]);//关闭管道2的写端char info[100];//写的内容char tmp[100];//读的内容while(1){cout << "(父进程)请输入内容到管道1中:";cin.getline(info, 100);write(fd1[1], info, sizeof(info));read(fd2[0], tmp, sizeof(tmp));cout << "(父进程)从管道2中读到的内容:" << tmp << endl;}}else if(pid == 0){//子进程:用管道1来接收数据,把内容修改后再用管道2发出去close(fd1[1]);//关闭管道1的写端close(fd2[0]);//关闭管道2的读端char info[100];//char tmp[100];while(1){//cout << "子进程正在等待父进程发消息..." << endl;read(fd1[0], info, sizeof(info));cout << "(子进程)从管道1中读到的内容为: " << info << endl;for(int i = 0; i < sizeof(info); ++i) info[i] = toupper(info[i]);cout << "(子进程)把修改后的数据发到管道2中。" << endl;write(fd2[1], info, sizeof(info));}}else{perror("fork");}return 0;
}

编译运行:

第11节课:有名管道(命名管道)—mkfifo函数

上面的7 8 9 10节课学的关于无名管道的内容,都是在一个.cpp文件中实现的,并且是父子进程之间的通信,即无名管道只能应用于有亲缘关系的进程之间的通信

本节课通过有名管道来实现无亲缘关系的进程之间的通信。

man 3 mkfifo

头文件:

#include<sys/types.h>
#include<sys/stat.h>


两个参数:
第一个参数是文件路径名;第二个参数是模式,确定FIFO的权限

有名管道其实是一个FIFO先入先出的文件,文件路径即第一个参数;
读端和写端都要打开,

返回值:函数调用成功(即是否成功创建有名管道)返回0;否则返回-1。

示例:参考链接使用管道实现进程间的通信(附C++实现代码)中FIFO双向通信代码实现
我们需要创建三个文件,将文件放到一个单独的文件夹中方便调试:一个用于创建命名管道,另外两个分别生成为模拟两个没有亲缘进程在该管道中进行数据的读取操作,具体代码如下:

文件1:create_named_pipe.cpp

#include<iostream>
#include<sys/stat.h>
using namespace std;int main(){//创建一个有名管道:文件路径为"my_fifo",权限为读写权限//string fileName;//cin >> fileName;//int res = mkfifo(fileName.c_str(), 0666);//0666的权限表示拥有读写权限int res = mkfifo("my_fifo", 0666);//0666的权限表示拥有读写权限if(res != 0)perror("mkfifo");return 0;
}

文件2:read_named_pipe.cpp

#include<iostream>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>#include<string>using namespace std;int main(){char buf[100] = {'\0'};//打开文件:int fd;//文件描述符fd = open("my_fifo", O_RDONLY);// fd = open("my_fifo", O_RDWR);//打开文件if(fd == -1)perror("open pipe error");//读取内容:while(1){cout << "准备从有名管道读取内容。。。" << endl;read(fd, buf, sizeof(buf));cout << "从管道中读到的内容为:" << buf << endl;//sleep(3);//读到内容之后停3秒之后继续读}close(fd);return 0;
}

文件3:write_named_pipe.cpp

#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<iostream>
#include<fcntl.h>
using namespace std;int main(){//打开文件:int fd;//文件描述符fd = open("my_fifo", O_WRONLY);if(fd == -1)perror("open pipe error");//写入内容:while(1){cout << "准备往有名管道写入内容。。。" << endl;string s;getline(cin, s);write(fd, s.c_str(), s.size() + 1);cout << "往管道中写入的内容为:" << s << endl;//sleep(3);//写入内容之后停3秒之后继续写}close(fd);return 0;
}

我们需要在两个shell里来测试该代码,运行两个读写文件生成可执行文件,在命令行g++ write_named_pipe.cpp -o write以及g++ read_named_pipe.cpp -o read生成两个可执行文件,并且在两个终端分别运行。
最终运行结果如下:

☆☆☆②任务间的通信 之 共享内存 shared memory

第12节课:共享内存 shared memory

示意图:

需要四个函数:
①创建共享内存空间:shmget()函数;
②建立映射:shmat()函数 ;
③解除映射:shmdt函数;
④删除共享内存空间:man shmctl
其中,shm表示shared memory共享内存,at表示attach连接,dt表示detach解绑,ctl表示control控制。

shmget()函数

man shmget


头文件:

#include<sys/ipc.h>
#include<sys/shm.h>

第一个参数:要创建出一个共享内存,就要把key值设置为IPC_PRIVATE类型;
第二个参数:创建出多大的共享内存;
第三个参数:常设置为IPC_CREATIPC_EXCL

返回值:如果创建成功,返回共享内存标识符shared memory identifier,简写shmid;否则返回-1。

shmat()函数 和 shmdt()函数

man shmat

shmat()函数:
三个参数:
第一个参数表示要把哪个共享内存映射到当前进程,即填写共享内存标识符shmid;
第二个参数表示要把共享内存映射到当前进程的哪一块地址处,即填写一个地址值,一般写NULL,系统就会选择一个合适的地方;
第三个参数表示权限,SHM_EXEC表示可执行权限;SHM_RDONLY表示只读权限,如果写个0就表示拥有读写权限;

返回值:返回的是void*类型,是个地址值,表示把共享内存映射到当前进程的位置的首地址const void *shmaddr,即对返回值所指向的内存进行操作,就等同于对共享内存的内容进行操作

shmdt()函数:
一个参数:const void *shmaddr,表示要解除共享内存的映射关系,要接触的共享内存在当前进程中的地址是shmaddr

shmctl函数
在解除共享内存和当前进程间的映射之后删除共享内存:

示例:

#include<sys/types.h>//fork函数、wait函数、mkfifo函数(有名管道)
#include<unistd.h>//fork函数、pipe函数
#include<sys/stat.h>//mkfifo函数(有名管道)
#include<sys/wait.h>//wait函数、waitpid函数
#include<pthread.h>//pthread_create函数(创建线程)
#include<fcntl.h>//read、open、write、close文件
#include<sys/ipc.h>//shmget函数
#include<sys/shm.h>//shmget函数
#include<string>
#include<cstring>
#include<iostream>
using namespace std;int main(){int shmID;//共享内存标识符shmID = shmget(IPC_PRIVATE, 1024, IPC_CREAT);//创建一个共享内存pid_t pid = fork();//创建子进程if(pid > 0){//父进程//将共享内存映射到父进程中:char* parent_addr = (char*)shmat(shmID, NULL, 0);//给共享内存写入数据:"Hi, I am Reus!\n"strcpy(parent_addr, "Hi, I am Reus!\n") ;//解除共性内存和父进程的映射关系:shmdt(parent_addr);waitpid(pid, NULL, 0);//父进程等子进程结束后再结束}else if(pid == 0){//子进程://将共享内存映射到子进程中:char* child_addr = (char*)shmat(shmID, NULL, 0);//从共享内存读取数据:cout << "从共享内存中读取的内容为:" << child_addr << endl;//解除共性内存和父进程的映射关系:shmdt(child_addr);}else    perror("fork");return 0;
}

编译运行:

第13节课:非亲缘关系进程通过共享内存通信

把上面的父子进程的内容分两个.cpp文件来写,单独放到一个文件夹中,不同的是:

示例:
read_shm.cpp文件

#include<sys/types.h>//fork函数、wait函数、mkfifo函数(有名管道)
#include<unistd.h>//fork函数、pipe函数
#include<sys/stat.h>//mkfifo函数(有名管道)
#include<sys/wait.h>//wait函数、waitpid函数
#include<pthread.h>//pthread_create函数(创建线程)
#include<fcntl.h>//read、open、write、close文件
#include<sys/ipc.h>//shmget函数
#include<sys/shm.h>//shmget函数
#include<string>
#include<cstring>
#include<iostream>
using namespace std;#define MY_KEY 9527
int main(){int shmID;//共享内存标识符shmID = shmget(MY_KEY, 1024, IPC_CREAT);//创建一个key值为9527的共享内存,如果已经存在那就找到此共享内存//将共享内存映射到进程2中:char* process2_addr = (char*)shmat(shmID, NULL, 0);//从共享内存读取数据:cout << "从共享内存中读取的内容为:" << process2_addr << endl;//解除共性内存和父进程的映射关系:shmdt(process2_addr);return 0;
}

write_shm.cpp文件

#include<sys/types.h>//fork函数、wait函数、mkfifo函数(有名管道)
#include<unistd.h>//fork函数、pipe函数
#include<sys/stat.h>//mkfifo函数(有名管道)
#include<sys/wait.h>//wait函数、waitpid函数
#include<pthread.h>//pthread_create函数(创建线程)
#include<fcntl.h>//read、open、write、close文件
#include<sys/ipc.h>//shmget函数
#include<sys/shm.h>//shmget函数
#include<string>
#include<cstring>
#include<iostream>
using namespace std;#define MY_KEY 9527int main(){int shmID;//共享内存标识符shmID = shmget(MY_KEY, 1024, IPC_CREAT);//创建一个key值为9527的共享内存,如果已经存在那就找到此共享内存//将共享内存映射到进程1中:char* process1_addr = (char*)shmat(shmID, NULL, 0);//给共享内存写入数据:strcpy(process1_addr, "Hi, I am Reus!\n") ;//解除共性内存和进程1的映射关系:shmdt(process1_addr);//删除共享内存//shmctl(shmID, IPC_RMID, NULL);return 0;
}

编译运行:
分别编译两个文件生成两个可执行文件:write_shmread_shm,然后在两个终端分别运行它们

上图中的绿色框中的内容就是一个进程往共享内存9527中写的内容,第一次没读到是因为另个一终端还没执行写入操作;

上图中的黄色框体中的-o命令表示输出的意思,用来指定可执行文件的名字,如果不指定,默认生成的是a.out文件;上面就是通过g++ read_shm.cpp -o read_shm来生成名为read_shm的可执行文件;

上图中的红色框体中的内容是将整个示例程序放在一个单独的文件夹中。

☆☆☆③任务间的通信 之 消息队列message queue

第14节课:消息队列message queue

通过消息队列实现进程间的通信,用的函数有
①msgget()函数:创建消息队列
②msgsnd()函数:消息队列的发送
③msgrcv()函数:消息队列的接收
④msgctl()函数:删除消息队列

msg = message snd = send

①msgget()函数

man msgget


头文件:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>

两个参数:
第一个是键值key,要创建出一个消息队列,就要把key值设置为IPC_PRIVATE类型;(补充:创建键值key的函数:ftok()函数
第二个是消息队列的标志位msgflg,常设置为IPC_CREATIPC_EXCL

返回值:如果创建成功,返回消息队列标识符message queue identifier,简写msgid;否则返回-1。

②msgsnd()函数 和 ③msgrcv()函数:

man msgsnd


msgsnd()函数:
第一个参数是消息队列表示符msgid,即上面的msgget函数的返回值;
第二个参数msgp是个指针,指向一个自定义的结构体,结构体的通用形式为

struct msgbuf{long mtype;        //消息类型:message type, must be > 0char mtext[1];    //消息内容:message data
}buff;//定义一个结构体变量buff

上面的结构体中消息类型mtype必须要有,消息内容message data并非只能是char数组,这里只是一个示例,即消息内容可以是char数组+int数据+…
第三个参数msgsz表示上面的结构体中消息内容的大小,即整个结构体的大小减去消息类型的大小;
第四个参数,给个默认值0

msgrcv()函数
第一个参数是消息队列表示符msgid,即上面的msgget函数的返回值;
第二个参数msgp是个指针,指向消息队列结构体
第三个参数msgsz表示上面的结构体中消息内容的大小;
第四个参数msgtyp表示上面结构体中消息类型
第五个参数,给个默认值0

④msgctl函数:
删除消息队列,用法和shmctl函数类似:

msgctl(msgid, IPC_RMID, NULL);//删除消息队列
shmctl(shmid, IPC_RMID, NULL);//删除共享内存

示例:

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<wait.h>
#include<iostream>
using namespace std;#define MSG_type 9527
int main(){int msgid = msgget(IPC_PRIVATE, IPC_CREAT);struct msgbuf{long mtype;string str;int num;};msgbuf buff;//结构体变量pid_t pid = fork();if(pid > 0){//父进程:发送消息while(1){sleep(2);buff.mtype = MSG_type;cout << "请输入字符串:";getline(cin, buff.str);//sleep(2);cout << "请输入一个数字:";cin >> buff.num;cin.get();//消除结束符msgsnd(msgid, &buff, sizeof(buff) - sizeof(long), 0);}waitpid(pid, nullptr, 0);}else if(pid == 0){//子进程:接收消息并打印while(1){cout << "正在等待父进程通过消息队列发过来的数据:" << endl;msgrcv(msgid, &buff, sizeof(buff) - sizeof(long), MSG_type, 0);//sleep(2);cout << "父进程发来的消息: str = " << buff.str << "; num = " << buff.num << endl;}msgctl(msgid, IPC_RMID, nullptr);//删除消息队列}else{perror("fork");}return 0;
}

编译运行:

第15节课:非亲缘关系进程通过消息队列通信

和共享内存差不多,也是要定义一个键值key,然后发送和接收都使用这个键值key:

send_msg.cpp

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<wait.h>
#include<string>
#include<iostream>
using namespace std;#define MSG_type 9527
#define my_key 1314
int main(){int msgid = msgget(my_key, IPC_CREAT);//ftokstruct msgbuf{long mtype;char str[100];int num;};msgbuf buff;//结构体变量while(1){sleep(2);buff.mtype = MSG_type;cout << "请输入字符串:";cin.getline(buff.str, 100);//getline(cin, buff.str);//sleep(2);cout << "请输入一个数字:";cin >> buff.num;cin.get();//消除结束符msgsnd(msgid, &buff, sizeof(buff) - sizeof(buff.mtype), 0);}return 0;
}

receive_msg.cpp

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<wait.h>
#include<iostream>
using namespace std;#define MSG_type 9527
#define my_key 1314
int main(){int msgid = msgget(my_key, IPC_CREAT);struct msgbuf{long mtype;char str[100];int num;};msgbuf buff;//结构体变量while(1){cout << "正在等待父进程通过消息队列发过来的数据:" << endl;msgrcv(msgid, &buff, sizeof(buff) - sizeof(buff.mtype), MSG_type, 0);//0//sleep(2);cout << "父进程发来的消息: str = " << buff.str << "; num = " << buff.num << endl;}msgctl(msgid, IPC_RMID, nullptr);//删除消息队列return 0;
}

编译运行:
依然是生成两个可执行文件send和receive,然后分别在两个终端上运行:

补充:创建键值key的函数:ftok()函数

整篇博客中涉及到key值的地方都是直接宏定义

#define MY_KEY 9527 共享内存的键值key
//#define MSG_type 9527 消息类型

实际应用中不这么写,容易造成冲突,而是用ftok()函数。

☆☆☆④任务间的同步 之 信号量semaphore

上面说的三种方式(管道pipe、共享内存shm、消息队列msg)属于任务之间的通信
接下来要讲的信号量是用来进行任务之间的同步

同步是指某个共享资源每次只能有一个人访问,它被访问的时候别人就不能访问,那么到底应该以什么顺序去访问这个资源呢,谁先谁后,这就是同步

(上面的内容复制下来的)
问:执行一次fork函数 先运行子进程还是先运行父进程?
答:(闫波)
没有顺序的
为了可以同步(同步就是有顺序),所以引入了信号量之类的东西;
执行同步就是让程序以一个确认的顺序运行,其实自己项目多线程用的多,包括咱们科研的,进程环境切换代价太大了,一般都是多线程
现在电脑也是,8核16线程这种,线程用的多,高并发也是线程
参考链接:Linux C++多线程同步的四种方式

------------------------------分割线---------------------------------

总之就是,通过信号量来控制 不同任务(进程) 或者 单个进程下的不同线程共享资源的访问(顺序)。

大概的示意图:

第16节课:无名信号量

信号量是个 >= 0整数

用到的函数:
①sem_init()函数:初始化一个信号量并通知系统该信号量会在进程间共享还是在单个进程中的线程间共享;
②sem_wait()函数:消耗1个信号量;
③sem_post()函数:发布1个信号量。

①sem_init()函数

man sem_init

头文件:

#include<semaphore.h>

注意:编译的时候要加-pthread

这个函数初始化一个无名信号量,
这个信号量在地址sem处,sem是一个sem_t *类型的变量,表示一个地址;(第一个参数
这个信号量的初始值是第三个参数value,它是一个无符号int型,即大于等于0的整数;
第二个参数pshared表示这个信号量是在进程间共享还是在某个进程下的线程之间共享
pshared为0,表示信号量在某个进程的的线程之间共享,并且信号量位于所有线程都能访问到的地方,比如全局变量,或者在堆里动态分配的变量;
pshared不为0,表示信号量在不同进程之间共享,并且信号量位于共享内存shm中,一共有三种方式创建共享内存:POSIX共享内存对象、System V共享内存段、使用mmap()函数创建的共享映射。下面的示例中使用第三种方式。

返回值:信号量初始化成功,就返回0;否则返回-1;

②sem_wait()函数:

man sem_wait


头文件:

#include<semaphore.h>

注意:编译的时候要加-pthread

这个函数是用来减少decrement信号量的个数的。
如果信号量的个数大于0,就让信号量的个数减一并且立刻返回;
如果信号量的个数为0,这个函数就会进入阻塞态,直到别的进程或者线程发布了信号量(即信号量的个数大于0)才会被唤醒;
参数就是上面的sem_init()函数的第一个参数sem_t*,即创建的信号量的地址
返回值:该函数调用成功,就返回0;否则返回-1;

③sem_post()函数:

man sem_post


头文件:

#include<semaphore.h>

注意:编译的时候要加-pthread

这个函数是用来增加increment信号量的个数的。
如果信号量的个数为0,就让信号量的个数加一,此时那些因为信号量个数为0而处于阻塞态的进程或者线程就会被唤醒;
参数也是是上面的sem_init()函数的第一个参数sem_t*,即创建的信号量的地址
返回值:该函数调用成功,就返回0;否则返回-1;

示例:
用fork函数创建父子进程:
父进程在一个while(1)循环中专门消耗信号量,每次消耗1个;
子进程刚开始消耗1个消耗量,
然后子进程在一个while(1)循环中每隔10秒发布2个信号量,紧接着再消耗1个信号量

因由于是进程间共享信号量,所以信号必须在共享内存中,示例中用mmap函数在当前进程中创建共享映射,所以想了解下mmap函数:

man mmap


头文件:

#include<sys/mman.h>

第一个参数设置为NULL,系统会自动分配一个空间来放这个共享映射;
第二个参数是创建的共享映射的长度,因为创建共享映射是为了放信号量,所以长度就设置为信号量的大小,即sizeof(sem_t)

返回值:如果成功创建了共享映射,就返回此映射区域的指针void*,将其强制转换为sem_init函数的第一个参数类型sem_t*,也就是信号量的地址;如果创建失败就返回-1。

示例中的写法:

sem_t* semAddr;//信号量的地址
//用mmap函数创建共享映射,信号量就在这里
semAddr = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
sem_init(semAddr, 1, 1);//第二个参数pshared为1,表示信号量在进程间共享,此时的信号量必须是共享内存区域中的某个位置的地址

代码:

#include<iostream>
#include<semaphore.h>
#include<sys/types.h>
#include<sys/mman.h>
#include<unistd.h>
using namespace std;
int main(){sem_t* semAddr;//信号量的地址//用mmap函数创建共享映射,信号量就在这里semAddr = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);sem_init(semAddr, 1, 3);//第二个参数pshared为1,表示信号量在进程间共享,此时的信号量必须是共享内存区域中的某个位置的地址//第三个参数表示信号量的个数,设置为1,表示刚开始有3个信号量pid_t pid;//fork函数的返回值pid = fork();if(pid > 0){//父进程:while(1){sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,父进程就会处于阻塞态,从而无法执行下一句cout << "这里是父进程...,此时信号量 大于0 ,因此父进程得以运行。" << endl;sleep(1);//让出共享资源}}else if(pid == 0){//子进程:sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,子进程就会处于阻塞态,从而无法执行下一句cout << "子进程..." << endl;sleep(1);//让出共享资源while(1){cout << "子进程每隔10秒发布2个信号量:(注意:这句没有消耗信号量!!!)" << endl;//这是子进程,sleep(10);//每隔5秒发布一个信号量sem_post(semAddr);//发布信号量sem_post(semAddr);//发布信号量//sleep(1);sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,子进程就会处于阻塞态,从而无法执行下一句cout << "子进程..." << endl;sleep(1);//让出共享资源}}return 0;
}

编译运行:
记得加-pthread

函数的功能:
用mmap函数创建一个共享映射,返回映射的地址,映射的大小是信号量的大小;
然后用sem_init函数初始化信号量,信号量的个数为3个,在进程间共享;
然后用fork函数创建父子进程:
父进程在一个while(1)循环中专门消耗信号量,每次消耗1个;
子进程刚开始消耗1个消耗量,
然后子进程在一个while(1)循环中每隔10秒发布2个信号量,紧接着再消耗1个信号量

上面的运行结果的解释:
刚开始有3个信号量,父子进程分别消耗2个和1个信号量;(红色剪头)
然后打印内容提示:每隔10秒子进程发布2个信号量;(黄色箭头,注意:打印操作不消耗信号量)
接下来就是每次发布2个消耗量,父子进程各消耗1个,一直循环下去…

第17节课:命名信号量

无关进程(没亲缘关系的进程)之间使用信号量来通信的话,就需要用到命名信号量

上面第16节课中无名信号量中的示例使用的是mmap函数创建一个共享映射,并返回此映射的地址,然后用这个地址作为初始化信号量sem_init时的第一个参数。
本节课用的函数有:
sem_open函数:创建信号量;
sem_wait函数:消耗信号量;
sem_post函数:发布信号量;
sem_close函数:删除进程和信号量之间的关联关系;

①sem_open函数

man sem_open


头文件:

#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>

这个函数用来 初始化一个新的命名信号量 或者 打开一个已经存在的命名信号量;

第一个参数是信号量的名称:const char*类型;
第二个参数O_CREAT
第三个参数是权限,0666表示读写权限;
第四个参数是信号量的个数;

返回值:如果函数调用成功就返回信号量的地址sem_t*,不同进程或者同一进程的不同线程就是通过访问这个值来消耗sem_wait或者发布sem_post信号量。

②sem_wait函数 ③sem_post函数 的使用方法见上节课

④sem_close函数

man sem_close


头文件:

#include <semaphore.h>

此函数用来删除进程和信号量之间的关联关系
参数是信号量的地址;
返回值:如果函数调用成功,返回0;否则返回-1;

示例:
进程1用sem_open创建一个信号量,初始化为3个;
进程1在一个while(1)循环中专门消耗sem_wait信号量,每次消耗1个;
进程2刚开始消耗sem_wait掉1个消耗量,
然后进程2在一个while(1)循环中每隔10秒发布sem_post2个信号量,紧接着再消耗1个信号量;
然后用sem_close函数用来删除进程和信号量之间的关联关系

代码:
process1.cpp

#include<iostream>
#include<semaphore.h>
#include<fcntl.h>
#include<sys/stat.h>
//#include<sys/types.h>
//#include<sys/mman.h>
#include<unistd.h>using namespace std;
int main(){sem_t* semAddr;//信号量的地址semAddr = sem_open("my_semaphore1", O_CREAT, 0666, 3);//打开一个已存在的信号量或者新建一个新的信号量,个数初始化为3个while(1){sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,进程1就会处于阻塞态,从而无法执行下一句cout << "这里是进程1...,此时信号量 大于0 ,因此父进程得以运行。" << endl;sleep(1);//让出共享资源}sem_close(semAddr);return 0;
}

process2.cpp

#include<iostream>
#include<semaphore.h>
#include<fcntl.h>
#include<sys/stat.h>
//#include<sys/types.h>
//#include<sys/mman.h>
#include<unistd.h>using namespace std;
int main(){sem_t* semAddr;//信号量的地址semAddr = sem_open("my_semaphore1", O_CREAT, 0666, 3);//打开一个已存在的信号量或者新建一个新的信号量,个数初始化为3个sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,进程2就会处于阻塞态,从而无法执行下一句cout << "进程2..." << endl;sleep(1);//让出共享资源while(1){cout << "进程2每隔10秒发布2个信号量:(注意:这句没有消耗信号量!!!)" << endl;//这是子进程,sleep(10);//每隔5秒发布一个信号量sem_post(semAddr);//发布信号量sem_post(semAddr);//发布信号量//sleep(1);sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,进程2就会处于阻塞态,从而无法执行下一句cout << "进程2..." << endl;sleep(1);//让出共享资源}sem_close(semAddr);return 0;
}

编译运行:
记得加-pthread

如果把进程2的先消耗1个信号量的代码屏蔽掉,那么第2次运行就可以正常的发布信号量了;
process2.cpp

#include<iostream>
#include<semaphore.h>
#include<fcntl.h>
#include<sys/stat.h>
//#include<sys/types.h>
//#include<sys/mman.h>
#include<unistd.h>using namespace std;
int main(){sem_t* semAddr;//信号量的地址semAddr = sem_open("my_semaphore1", O_CREAT, 0666, 3);//打开一个已存在的信号量或者新建一个新的信号量,个数初始化为3个 // sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,进程2就会处于阻塞态,从而无法执行下一句// cout << "进程2..." << endl;// sleep(1);//让出共享资源while(1){cout << "进程2每隔10秒发布2个信号量:(注意:这句没有消耗信号量!!!)" << endl;//这是子进程,sleep(10);//每隔5秒发布一个信号量sem_post(semAddr);//发布信号量sem_post(semAddr);//发布信号量//sleep(1);sem_wait(semAddr);//消耗信号量,信号量大于0时让信号量减一,并执行下一句;当信号量为0时,进程2就会处于阻塞态,从而无法执行下一句cout << "进程2..." << endl;sleep(1);//让出共享资源}sem_close(semAddr);return 0;
}

然后再编译运行,此时等待10秒后,进程2发布两个信号量,然后两个进程就不再是阻塞态了。

第18节课:线程间信号量同步

上面的第16、17节课分别是父子进程间共享无名信号量;无亲缘关系的进程间共享命名信号量;本节课讲同一进程的不同线程间共享无名信号量

大致回顾下sem_init()函数介绍:

头文件:

#include<semaphore.h>

注意:编译的时候要加-pthread

这个函数初始化一个无名信号量,
第一个参数是信号量的地址;
第二个参数表示这个信号量是在进程间共享还是在某个进程下的线程之间共享

  • pshared为0,表示信号量在某个进程的的线程之间共享,并且信号量位于所有线程都能访问到的地方,比如全局变量,或者在堆里动态分配的变量
  • pshared不为0,表示信号量在不同进程之间共享,并且信号量位于共享内存shm中,一共有三种方式创建共享内存:POSIX共享内存对象、System V共享内存段、使用mmap()函数创建的共享映射。下面的示例中使用第三种方式。

第三个参数是信号量的个数;
返回值:信号量初始化成功,就返回0;否则返回-1;

------------------------------分割线-----------------------------------

如果是进程间共享信号量,第二个参数就写1,然后有三种方式可以在创建一个共享内容,用来存放信号量,这样各个进程就可以访问到信号量,从而实现进程间的通信。例如:

sem_t* semAddr;//信号量的地址
//用mmap函数创建共享映射,信号量就在这里
semAddr = (sem_t*)mmap(NULL, sizeof(sem_t), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
sem_init(semAddr, 1, 3);//第二个参数是1,表示该信号量是进程间共享,第三个参数表示信号量有3个

如果是同一进程的不同线程间共享信号量,第二个参数就写0,此时信号量应该位于所有线程都能访问到的地方,比如全局变量,或者在堆里动态分配的变量

示例:(和前两节课的例子差不多)
用sem_init(&sem, 0, 3);初始化一个信号量,个数为3个;
线程1在一个while(1)循环中专门消耗sem_wait信号量,每次消耗1个;
线程2刚开始消耗sem_wait掉1个消耗量,
然后进程2在一个while(1)循环中每隔10秒发布sem_post2个信号量,紧接着再消耗1个信号量

代码:

#include<semaphore.h>
#include<unistd.h>
#include<pthread.h>
#include<iostream>
using namespace std;void* pthread1_func(void *);
void* pthread2_func(void *);int count = 0;
sem_t sem;//信号量为全局变量int main(){pthread_t pthread1, pthread2;//线程号int res;//初始化信号量:sem_init(&sem, 0, 3);//第二个参数为0表示线程间共享信号量,信号量的个数初始化为3个//创建线程:res = pthread_create(&pthread1, nullptr, pthread1_func, nullptr);if(res == 0) cout << "pthread1创建成功!" << endl;else perror("pthread_create");res = pthread_create(&pthread2, nullptr, pthread2_func, nullptr);if(res == 0) cout << "pthread2创建成功!" << endl;else perror("pthread_create");//线程合并,也即结束线程:pthread_join(pthread1, nullptr); cout << "pthread1结束了" << endl;pthread_join(pthread1, nullptr); cout << "pthread2结束了!" << endl;return 0;
}void* pthread1_func(void *){while(1){sem_wait(&sem);//消耗1个信号量cout << "pthread1-(" << ++count << ")" << endl;//消耗第 " << ++count << " 个信号量。" << endl;sleep(1);}return nullptr;
}
void* pthread2_func(void *){sem_wait(&sem);//消耗1个信号量cout << "pthread2-(" << ++count << ")" << endl;//消耗第 " << ++count << " 个信号量。" << endl;//sleep(1);while(1){cout << "进程2每隔5秒发布2个信号量(注意:这条内容不消耗信号量)" << endl;//sleep(5);sem_post(&sem);//发布1个信号量sem_post(&sem);//发布1个信号量sem_wait(&sem);//消耗1个信号量cout << "pthread2-(" << ++count << ")" << endl;//消耗第 " << ++count << " 个信号量。" << endl;sleep(1);}return nullptr;
}

编译运行:
(记得加-pthread

☆☆☆⑤任务间的同步 之 互斥锁mutex

第19节课:互斥锁(常用在线程间的同步)

//先安装两个文件:
sudo apt-get install glibc-doc
sudo apt-get install manpages-posix-devman pthread_mutex_init


头文件:

#include<pthread.h>

要用到的函数:
pthread_mutex_init函数:初始化互斥锁
pthread_mutex_lock函数:给共享资源加锁
pthread_mutex_unlock函数:给共享资源解锁

互斥锁mutex用来保护共享数据;
一个互斥锁mutex只有两种状态:解锁状态(不被任何线程占用) 和 加锁状态(只能被一个线程占有);
一个互斥锁mutex不会被两个不同的线程同时占有;
如果线程1尝试给某个(已经被线程2上了锁的)共享资源加锁,那么线程1将被挂起(suspended)直到线程2解锁之后才行。

第一个参数pthread_mutex_t*类型,表示互斥锁的地址;
第二个参数:通常写NULL;
返回值:pthread_mutex_init 函数总是返回0,其他几个函数在函数调用成功时返回0,调用失败时返回非零。
pthread_mutex_init always returns 0. The other mutex functions return 0 on success and a non-zero error code on error.

示例:(和上节课的例子相似)
用pthread_mutex_init初始化一个互斥锁;
线程1在一个while(1)循环中先加锁,然后打印hello world,然后解锁,然后sleep(1)让出系统资源;
线程2刚开始
先sleep(1)(让线程1先运行),然后也在一个while(1)循环中先加锁,然后打印good morning,然后解锁,然后sleep(1)让出系统资源;

代码:

#include<semaphore.h>
#include<unistd.h>
#include<pthread.h>
#include<iostream>
using namespace std;void* pthread1_func(void *);
void* pthread2_func(void *);//int count = 0;
//sem_t sem;//信号量为全局变量
pthread_mutex_t my_mutex;//互斥锁为全局变量int main(){pthread_t pthread1, pthread2;//线程号int res;//初始化互斥锁:pthread_mutex_init(&my_mutex, nullptr);//创建线程:res = pthread_create(&pthread1, nullptr, pthread1_func, nullptr);if(res == 0) cout << "pthread1创建成功!" << endl;else perror("pthread_create");res = pthread_create(&pthread2, nullptr, pthread2_func, nullptr);if(res == 0) cout << "pthread2创建成功!" << endl;else perror("pthread_create");//线程合并,也即结束线程:pthread_join(pthread1, nullptr); cout << "pthread1结束了" << endl;pthread_join(pthread1, nullptr); cout << "pthread2结束了!" << endl;return 0;
}void* pthread1_func(void *){while(1){pthread_mutex_lock(&my_mutex);//加锁for(int i = 0; i < 5; ++i){cout << "hello world :" << i + 1 << "; ";//打印5次 pthread1sleep(1);} cout << endl;pthread_mutex_unlock(&my_mutex);//记得解锁sleep(1);//记得让出系统资源}return nullptr;
}
void* pthread2_func(void *){sleep(1);//先让出系统资源,让线程1先运行while(1){pthread_mutex_lock(&my_mutex);//加锁for(int i = 0; i < 5; ++i){cout << "good morning :" << i + 1 << "; ";//打印5次sleep(1);} cout << endl;pthread_mutex_unlock(&my_mutex);//记得解锁sleep(1);//记得让出系统资源}return nullptr;
}

编译运行:

☆☆☆⑥内核和应用进程间/进程和进程间 传递的控制命令 之 信号signal

第20节课:信号signal

信号区别于之前的信号量semaphore;
信号是指内核和应用进程之间以及应用进程和应用进程之间传递的一些控制命令,很少用信号来传递数据。

在终端运行程序时如果想要结束运行,键盘上按Ctrl+c程序就会停止,就是给当前进程发送一个信号,让它停止。
(补充:之前遇到过按Ctrl+c只是父进程停止了,但子进程还在运行的情况,这是因为Ctrl+c是发送给父进程的,并没有发给子进程,所以子进程一直在运行。如果停止子进程,打印子进程的pid,假如说是2997,然后在另一个终端输入指令kill 2997,就可以删除了。)

查看Linux支持哪些信号:

kill -l


共64种信号,上面说的Ctrl+c属于第二种:SIGINT

本节课会用到的函数:
①kill函数;
②signal函数。

①kill函数

man 2 kill


头文件:

#include<sys/types.h>
#include<signal.h>

kill函数的功能:发送任意信号给任意进程组或者进程。
第一个参数是进程号pid

  • 如果pid是正数,就把信号发给进程ID为pid的进程;
  • 如果pid是0,就把信号发给当前进程组的每个进程;
  • 如果pid是-1,就把信号发给所有允许发送信号的进程,除了process 1;

第二个参数是要发送的信号signal,共64种。

示例1:(kill函数)
创建父子进程, 子进程每隔1秒打印一句话,父进程等待十秒后,给子进程发送一个信号SIGKILL,把子进程停止;然后父进程也结束了。

代码:

#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
#include<iostream>
using namespace std;
int main(){pid_t pid;pid = fork();//fork的返回值:父进程返回子进程的pid;子进程返回0。if(pid > 0){//父进程cout << "父进程的ID号:" << getpid() << endl;sleep(10);kill(pid, SIGKILL);//给子进程发送SIGKILL信号}else if(pid == 0){//子进程int count = 0;cout << "子进程的ID号:" << getpid() << endl;while(1){cout << "子进程运行时间:" << ++count  << " 秒..." << endl;sleep(1);}}else   perror("fork");return 0;
}

编译运行:

②signal函数

man signal

头文件:

#include<signal.h>

具体的函数用法可以看看下面的几篇博客:
博客1:signal函数中的2. signal信号处理机制
博客2:Signal ()函数详细介绍 Linux函数;
博客3:【Linux函数】Signal ()函数详细介绍;

(博客1)
2.signal信号处理机制
可以用函数signal注册一个信号捕捉函数
函数原型为:

#include<signal.h>
typedef void (*sighandler_t)(int);//信号捕捉函数
sighandler_t signal(int signum, sighandler_t handler);

第1个参数signum表示要捕捉的信号
第2个参数是个函数指针,表示要对该信号进行捕捉的函数,该参数也可以是SIG_DFL(表示交由系统缺省default处理,相当于白注册了)或SIG_IGN(表示忽略ignore掉该信号而不做任何处理)。
返回值:signal如果调用成功,返回以前该信号的处理函数的地址,否则返回 SIG_ERR

sighandler_t是信号捕捉函数,由signal函数注册,注册以后,在整个进程运行过程中均有效,并且对不同的信号可以注册同一个信号捕捉函数。该函数只有一个参数,表示信号值。

看下面三个示例:

  1. 捕捉终端CTRL+c产生的SIGINT信号:
#include <unistd.h>
#include<iostream>
#include<signal.h>
#include <sys/types.h>
using namespace std;
//信号捕捉函数:
void SignHandler(int iSignNo)
{//printf("Capture sign no:%d/n",iSignNo); cout << "Capture sign no:" << iSignNo << endl;
}int main()
{cout << "当前进程的ID号为:" << getpid() << endl;signal(SIGINT,SignHandler);//捕获int count = 0;while(true){cout << "count " << ++count << endl;sleep(1);}return 0;
}

解释:
该程序运行起来以后,通过按 CTRL+c将不再终止程序的运行。因为CTRL+c产生的SIGINT信号已经由进程中注册的SignHandler函数捕捉了。
该程序可以通过 Ctrl+/终止,因为组合键Ctrl+/能够产生SIGQUIT信号,而该信号的捕捉函数尚未在程序中注册。(这句话试了之后没有用,最后还是在另外一个终端输入kill 进程号才结束的)

编译运行:

  1. 忽略掉终端CTRL+c产生的SIGINT信号:
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<iostream>
using namespace std;
int main(){cout << "当前进程的ID号为:" << getpid() << endl;signal(SIGINT, SIG_IGN);//忽略int count = 0;while(1){cout << "count " << ++count << endl;sleep(1);}return 0;
}

该程序运行起来以后,将CTRL+C产生的SIGINT信号忽略掉了,所以CTRL+C将不再能是该进程终止,要终止该进程,可以在另一个终端输入kill 进程号

编译运行:

  1. 接受信号的默认处理,接受默认处理就相当于没有写信号处理程序:
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
#include<iostream>
using namespace std;
int main(){cout << "当前进程的ID号为:" << getpid() << endl;signal(SIGINT, SIG_DFL);//缺省,默认int count = 0;while(1){cout << "count " << ++count << endl;sleep(1);}return 0;
}

编译运行:

纠正一下:博客里写错了,不是Ctrl+/,而是Ctrl+\,这才是SIGQUIT信号的意思:

(博客2)

Signal Description
SIGABRT 由调用abort函数产生,进程非正常退出
SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS 某种特定的硬件异常,通常由内存访问引起
SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用
SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGEMT 和实现相关的硬件异常
SIGFPE 数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送
SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL 非法指令异常
SIGINFO BSD signal由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO 异步IO事件
SIGIOT 实现相关的硬件异常,一般对应SIGABRT
SIGKILL 无法处理和忽略。中止某个进程
SIGLWP 由Solaris Thread Libray内部使用
SIGPIPE 在reader中止之后写Pipe的时候发送
SIGPOLL 当某个事件发送给Pollable Device的时候发送
SIGPROF Setitimer指定的Profiling Interval Timer所产生
SIGPWR 和系统相关。和UPS相关。
SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV 非法内存访问
SIGSTKFLT Linux专用,数学协处理器的栈异常
SIGSTOP 中止进程。无法处理和忽略。
SIGSYS 非法系统调用
SIGTERM 请求中止进程,kill命令缺省发送
SIGTHAW Solaris专用,从Suspend恢复时候发送
SIGTRAP 实现相关的硬件异常。一般是调试异常
SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU 当Background Group的进程尝试写Terminal的时候发送
SIGURG 当out-of-band data接收的时候可能发送
SIGUSR1 用户自定义signal 1
SIGUSR2 用户自定义signal 2
SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING Solaris Thread Library内部实现专用
SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU 当CPU时间限制超时的时候
SIGXFSZ 进程超过文件大小限制
SIGXRES Solaris专用,进程超过资源限制的时候发送

(博客3)
博客3的内容基本就是从博客1和2中整理的内容整合到一起,写的更清晰好看一些。

补充:
The signals SIGKILL and SIGSTOP cannot be caught or ignored.//SIGKILL和SIGSTOP信号不能被捕获或者忽略。
有的信号是允许修改的,但有的信号是不能修改和屏蔽的,只能按照系统的设定去执行。

☆☆☆⑦socket套接字

这部分内容属于网络编程的范围,具体就是TCP、UDP的实现,见下面的 五、Linux网络编程

五、Linux网络编程

点这里

①Linux简明系统编程(嵌入式公众号的课)---总课时12h相关推荐

  1. Linux简明系统维护手册

    Linux简明系统维护手册 摘要     本文是长期工作经验的总结,说明了Linux下各种主要网络应用的配置实例,对接触Linux时间不长的同学们很有参考价值.注意:其中有些应用还是很复杂的呀.文中V ...

  2. 公众号网课查题-掘光者题库系统

    公众号网课查题-掘光者题库系统 本平台优点: 多题库查题.独立后台.响应速度快.全网平台可查.功能最全! 1.想要给自己的公众号获得查题接口,只需要两步! 2.题库: 题库:题库后台(点击跳转) 题库 ...

  3. 公众号网课搜题系统-掘光者题库

    公众号网课搜题系统-掘光者题库 本平台优点: 多题库查题.独立后台.响应速度快.全网平台可查.功能最全! 1.想要给自己的公众号获得查题接口,只需要两步! 2.题库: 题库:题库后台(点击跳转) 题库 ...

  4. 外卖优惠券返利系统外卖返利公众号搭建cps系统小程序SaaS源码

    外卖优惠券返利系统外卖返利公众号搭建cps系统小程序SaaS源码 美团/饿了么外卖CPS联盟返利公众号小程序裂变核心源码 源代码地址 https://gitee.com/caonima008/coup ...

  5. 【Linux】系统编程之文件(标准I/O库)

    目录 一.文件I/O与标准I/O的区别(open与fopen) 1.来源 2.移植性 3.适用范围 4.文件IO层次 5.缓冲 二.函数fopen.fwrite.fread.fseek.fclose ...

  6. 公众号网课搜题接口系统调用搭建

    公众号网课搜题接口系统调用搭建 本平台优点: 多题库查题.独立后台.响应速度快.全网平台可查.功能最全! 1.想要给自己的公众号获得查题接口,只需要两步! 2.题库: 题库:题库后台(点击跳转) 题库 ...

  7. Linux/Unix系统编程手册 第三章:系统编程概念

    本章介绍系统编程的基础概念和一些后续章节用到的函数及头文件,并说明了可移植性问题. 系统调用是受控的内核入口,通过系统调用,进程可以请求内核以自己的名义去执行某些动作,比如创建子进程,执行I/O操作, ...

  8. 基于IdentityServer的系统对接微信公众号

    业务需求 公司有两个业务系统,A和B,AB用户之间属于多对一的关系,数据库里面也就是两张表,A表有个外键指向B.现在需要实现以下几个功能. A用户扫描B的二维码,填写相关的注册信息,注册完成之后自动属 ...

  9. LINUX C系统编程与PYTHON中的时间模块对比

    今天看python时间模块time的时候发现和LINUX系统编程中的时间调用函数基本一样,以前刚好没有好好学习LINUX C编程的时间模块就对比进行了学习. 本文只是给出函数接口和使用方式,详细了解请 ...

最新文章

  1. 1925亿美元,中国仍是世界最大芯片市场!2022全球半导体行业报告出炉
  2. webApp移动开发之REM
  3. web安全编程——权限的分配和控制
  4. Docker-Compose命令详解
  5. 自制H3C交换机CONSOLE线
  6. C语言---链表的基本应用
  7. SQLite 3.31.0 发布,世界上使用量最大的数据库引擎
  8. 4-2 面向复用的软件构造技术
  9. C盘不能新建文件的问题解决办法
  10. oracle无效的关系运算符_每日一课 | Java 8中的instanceof运算符和访客模式替换
  11. HALCON:图像采集之同步采集(synchronous)与异步采集(asynchronous)
  12. Windows10下VB6.0开发——常用的字符串处理函数工具
  13. ios开发--清理缓存
  14. Atitit 从api的使用区分工程师级别 高级 中级 初级工程师常使用的api与框架类库 目录 1. 初级工程师使用的api和框架类库ssm 1 2. 中级工程师常使用的api和框架类库 1 3.
  15. Python黑帽子——通过Paramiko使用SSH
  16. 程序员做饭指南,GitHub教程来了
  17. python语言例子_第一个Python实例
  18. Direct2D (9) : 显示图像
  19. 0622_ArcMap添加地图地图(矢量底图与影像地图)_太乐地图插件ArcTailer.tlb
  20. 调研分析:全球与中国多媒体投影仪镜头市场现状及未来发展趋势

热门文章

  1. java 实现汉字转换拼音_Java实现汉字转换为拼音
  2. Thinkphp 表名下滑杠处理
  3. 恭喜 SphereEx 联合创始人潘娟成为亚马逊云科技新晋 Data Hero
  4. C语言一球从100米高度自由落下,每次落地后反跳回原高度的一半;再落下,求它在第10次落地时,共经过多少米?
  5. CM311-3_YS_晨星MSO9385芯片刷机分享
  6. PWNFEST黑客大会:苹果Safari与微软Edge浏览器均被攻破
  7. 小鱼的数字游戏递归解
  8. lambda表达式和Stream
  9. springboot热部署
  10. # cmake --version -bash: /usr/bin/cmake: Too many levels of symbolic links