牛客C++笔记——linux多进程开发

Linux多进程开发

进程概述

程序和进程

  1. 程序是包含一些机器语言、程序入口、数据、共享库和链接库的文件,用来描述如何在运行时候创建一个进程。
  2. 进程就是正在运行的程序的实例 ,是内核定义的抽象的实体,是操作系统的基本分配单元(占用内存和cpu)、执行单元, 由用户内存空间和一系列内核数据结构组成
  3. 程序就是文件,占用磁盘,一个程序文件可以创建多个进程

单道、多道程序设计

  1. 单道,cpu中只有一个程序在运行
  2. 多道,通过管理程序控制,使得cpu同时穿插运行多个程序,宏观上看同时运行,微观上还是一时刻运行一个,提高cpu利用率。

时间片

  1. 时间片是操作系统内核为进程调度分配的一段cpu时间,一个cpu上穿插运行着多个进程,每个进程占一定的时间片。
  2. 时间片也不能随便切,很小的话利用率也不一定高
  3. cpu其实在一个时刻都只能运行一个指令,只是切片了,穿插着人的层面看来同时运行

并行和并发

  1. 并行,同一时刻,多条指令在多个处理器上同时运行(两队人同时使用两台咖啡机)
  2. 并发,cpu其实在一个时刻都只能运行一个指令,只是切片了,穿插着运行,人的层面看来同时运行(两队人交替使用一台咖啡机)

进程控制块(PCB)

  1. 为了管理每个进程,操作系统内核会给每个进程分配一个PCB,存放着这个进程的一些信息,在内核中是一个结构体,放在内核区中
  2. 信息包括:进程id(唯一,非负整数)、进程状态、cpu时间、一些寄存器数据、文件描述符表、当前工作路径、描述终端控制的信息、用户id和组id、资源上限等

进程状态

  1. 三态模型
    (1) 就绪态:操作系统为进程分配到除cpu外的各项资源之后的状态
    (2)运行态:操作系统为进程分配到cpu资源之后,就进入运行态,如果进程的一个时间片执行完之后就转换成就绪态。
    (3)阻塞态:如果一个进程sleep或者等待用户,就是停住了,就是阻塞态,阻塞结束后进入就绪态
  2. 五态模型:就是在三态模型之外还有新建态、终止态
    (1) 新建态:就是进程刚刚创建,还未进入就绪队列,就是新建态
    (2) 终止态:进程执行完毕、遇到错误退出、被操作系统终止、被其他有权终止的进程终止掉,就进入终止态 ,进入终止态后,等待操作系统来处理删除进程。

进程相关指令

ps aux 或者 ps ajx
# a:显示终端上的所有进程
# u:显示进程的详细信息
# x:显示没有控制终端的进程
# j:列出与作业控制相关的信息

PID:进程的ID,,整数,范围是0~36767,进程号唯一,终止后会重新分配给其他用
PPID: 父进程的ID
PGID:进程组的ID(几个进程一个组成进程组)
SID:会话的ID(几个进程组组成一个会话)
TTY:进程所处的终端
STAT的参数意义
TOP命令

在这里插入图片描述

进程号

  1. 每个进程都有一个号来标识,唯一,被删除后会分配给其他用
  2. 任何进程(除init进程)之外,都是由另一个进程创建,叫父进程,终端也是一个进程,终端中运行一个程序,那他就创建了一个进程
  3. 父进程需要负责回收子进程的资源
  4. 每个进程结束后都会自己释放用户空间,但是内核区需要父进程来释放

进程创建

  1. 使用man fork可以查看fork的文档
  2. 使用:
    unistd.h为Linux/Unix系统中内置头文件,包含了许多系统服务的函数原型,例如read函数、write函数和getpid函数等。其作用相当于windows操作系统的"windows.h",是操作系统为用户提供的统一API接口,方便调用系统提供的一些服务。
#include <sys/types.h>
#include <unistd.h>
//pid_t fork(void);
//返回值:
//	成功:子进程中返回0,父进程中返回子进程ID
//	失败:返回-1,失败原因——1.进程数达到上限,2.系统内存不足
int main(){
pid_t pid = fork();//进程从这里开始出现分支
if(pid > 0) printf("这是父进程会执行的代码");
if(pid == 0) printf("这是子进程会执行的代码");
printf("这是父进进程和子进程都会执行的代码");
}
  1. 父进程创建子进程之后,用户内存空间是拷贝了一份过去的,所以一开始它们拥有的值一样,但后续操作中,两个进程之间互不影响,他们空间是独立的。
  2. 上面说的拷贝,不是创建完就新开辟一个原样的物理内存空间,这样有浪费,他是写时拷贝
  3. 写时拷贝,就是刚创建时候,子进程和父进程其实还是映射着同一个物理内存地址的,但一个进程对这个地方进行写操作时候,才会新开辟一个物理内存空间。这样的目的是为了降低内存消耗和拷贝内存的时间
  4. 创建子进程之后,内核区也是拷贝过去的,里面除了进程管理块(PCB)里面的进程号(pid)东西不一样,其他比如文件描述符那些是一样的
    在这里插入图片描述
    在这里插入图片描述

GDB调试多进程

在这里插入图片描述

exec函数族

  1. exec族函数是用来在一个程序中再启动一个可执行程序,取代原来的进程中的内容,用户区的数据被取代
  2. 如果启动成功,原来的进程已经被取代了,也就没有返回一说了,如果启动失败,返回-1,原进程继续往下执行
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
//path;可执行程序的路径
//arg:可执行程序的参数,参数需要以null结尾(哨兵)
//返回值:只有调用失败的时候才会返回-1,其他没有
  1. 举例
    hello程序,编译生成hello可执行程序
#include <unistd.h>
#include <stdio.h>
int main(){
	printf("hello");
}

调用程序

#include <unistd.h>
#include <stdio.h>
int main(){
    pid_t pid = fork();
    if(pid > 0) 
        printf("father process");
    else{
        execl("hello", "hello", NULL);//第一个为路径,后面的为参数,第一个参数得填,其实没什么用,参数需要以NULL结尾
        printf("original child process\n");
    }
}
  1. 也可用用来执行linux的一些系统命令,它们其实也是可执行程序,比如ps aux、ls等等,
    通过whic ps 就可以查看到ps在哪
    在这里插入图片描述
execl("/bin/ps","ps","aux",NULL)
  1. execlp
    这个相比于excel,他可以去环境变量里面寻找可执行程序,比如就可以这样用
execlp(("ps","ps","aux",NULL);

进程控制

进程退出

  1. _exit()是linux的库函数
  2. exit()是c库函数
    在这里插入图片描述
#include <stdlib.h>
void exit(int status);//这个是c库的函数,status是设置要返回的退出状态
#include <unistd.h>
void _exit(int status);//这个是linux的函数,status是设置要返回的退出状态

孤儿进程

  1. 父进程提前结束之后,子进程还在运行,那他就叫孤儿进程
  2. 出现这样情况时,孤儿进程就交给init() 进程处理,孤儿进程结束之后,它来回收孤儿的资源
  3. 孤儿进程是没有什么危害的

僵尸进程

  1. 就是进程结束之后,但是父进程没有进行回收释放,他就占用着空间和进程号,它就是一个僵尸进程。
  2. 他都不能被kill -9 杀掉,危害很大,可能导致它占着号,占太多满了后面可能就开不了新进程了。
  3. 如果此时父进程结束,那僵尸进程就会变成孤儿进程
  4. 解决方式就是在父进程中用wait()或者waitpid()得到他的退出状态并彻底清除这个进程。
  5. wait()和waitpid()功能差不多,wait()是阻塞的,调用一次处理一个子进程,waitpid是非阻塞的,并且可以指定等待的进进程号
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
//它会等待任意一个子进程结束,任意一个子进程状态变化之后就去执行释放
//参数:传入一个指针,可以获取到子进程状态
//返回值: 1.返回被释放的进程号,2.没有子进程可以释放了或者调用失败返回-1
pid_t waitpid(pid_t pid, int *wstatus, int options);
//参数:
//	pid:
//		pid > 0: 指定子进程的pid
//		pid = 0:回收当前进程组内的所有子进程
//		pid = -1:回收所有的子进程,相当于wait()
//		pid < -1: 要回收指定进程组的pid的绝对值,回收那个指定进程组中的子进程
// 	*wstatus: 获取子进程结束的状态
// 	options: 0-阻塞,WNOHANG-非阻塞

在这里插入图片描述

在这里插入图片描述

进程间通信(IPC)

基本概念

  1. 进程之间使用的资源是独立的,不能直接去访问其他进程的资源
  2. 但是进程不是孤立的,它们可以进行信息和状态的交互
  3. 进程之间通信的作用:数据传输、通知事件、资源共享、进程控制
    4.这里记录一个概念:进程间资源共享的时候,需要同步,这个同步的概念理解是,比如医院看病,一个人看完另一个再去,这叫同步,俩人一起去看病那叫异步。

Linux进程间通信的方式

在这里插入图片描述

管道

管道(匿名管道)概述

在这里插入图片描述
这上面哪个 | 叫做管道符,他的原理就是在当前终端创建一个管道,连接ls 和 wc 两个进程

  1. 管道是内核空间 中维护的缓冲区,是有限的,不同操作系统不一样
  2. 管道拥有文件的特质:读、写操作,管道的两端对应着文件描述符
  3. 匿名管道没有文件实体,有名管道有文件实体,但是也只是在内存中的,不会在磁盘上有一个实际的文件
  4. 管道是一个字节流,不需要定义通信协议,可以从管道中读取任意大小的数据
  5. 管道传递的数据是顺序的,先进先出
  6. 管道是半双工的,就是同一时刻只能一端发,另外一端收,不能同时刻两边都发都收。
  7. 从管道读取数据时一次性操作,一旦被读走,就从管道中被抛弃了
  8. 匿名管道只能在有公共祖先(父进程与子进程、兄弟进程等)的进程间使用
    在这里插入图片描述
  9. 由于子进程与父进程之间其实是有一样的文件描述符,所以它们之间可以通过文件交换数据,同时,管道又是有文件的特性,所以管道就可以用于进程之间的通信。
  10. 同样由于 上面10说的特性,只有有亲缘关系的进程之间它们才满足这样的条件,所以就是如8所说
    在这里插入图片描述
  11. 管道的数据结构,是环形队列
    在这里插入图片描述

使用匿名管道进行进程间通信

在这里插入图片描述
在这里插入图片描述
子进程和父进程两个都去管道里面读取数据的时候,就是第二种情况,有可能出现自己发的数据自己又去读回来,这样是不对的,所以一般只用第三种情况,一边发一边收,这样比较合适。
在这里插入图片描述

  1. 当管道以阻塞模式运行时候
    读管道:
    管道中有数据:就返回读到的字节数
    管道中无数据:
    管道写端都被完全关闭,那就返回0,相当于读到了文件末尾
    管道写端没有完全关闭,那就是阻塞,直到有数据进来
    写管道:
    管道读端被完全关闭:就会收到一个SIGPIPE信号,进程异常终止
    管道读端没有被完全关闭:
    管道已满:阻塞,直到管道内有空间写入
    管道未满:写入数据,并返回实际写入的字节数

  2. 可以通过fcntl函数设置文件描述符的模式,使他变成非阻塞的,这样管道就是非阻塞的了,当非阻塞的时候,如果没有数据,read返回的就是-1。

有名管道

  1. 匿名管道只能在有关系的进程之间通信
  2. 有名管道(FIFO),也叫命名管道、FIFO文件
  3. 有名管道是以文件形式存在于文件系统中的,在磁盘上能看到文件,但是里面是不存数据的,管道的数据都是在内存中(内核区)的,他的打开方式和打开一个普通文件一样,因此即使无关的的进程也能通过FIFO交换数据
  4. 对他的操作其实就是跟普通文件、匿名管道操作一样,数据读取顺序也是先入先出,FIFO(就是先入先出的意思)
  5. 使用FIFO的进程退出后,FIFO文件继续保存在文件系统中以便以后使用
  6. FIFO是有名字的,不同的进程都可以打开管道进行通信

有名管道的使用

在这里插入图片描述
在这里插入图片描述
一般也是,管道设置成一端只写,另一端只读,这样比较好
在这里插入图片描述

用有名管道实现一个聊天功能

  1. 实现思路,实现半双工
    在这里插入图片描述

内存映射

  1. 内存映射也是一种进程间通信的方式,用内存映射磁盘上面的空间,不同进程映射同一块文件,这样一个进程修改了文件,另一个进程就能相应同步改变
  2. 用内存映射的方式效率比较高的
    在这里插入图片描述

mmp内存映射

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

port的一定要有读权限,才能操作文件,port的权限要小于等于open的权限

munmap释放内存映射

在这里插入图片描述

使用内存映射进行进程间通信

  1. 使用内存映射进行通信,不仅可以在父子进程之间,也可以在不想关的进程之间
    在这里插入图片描述

使用内存映射的注意事项

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

信号

信号基本概念

  1. 信号是linux比较古老的一种通信机制,理解为软件中断,一个进程执行过程中,被另外一个异步的进程中断,转去执行另外一个突发事件。 在这里插入图片描述
    在这里插入图片描述

常见的一些信号

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

信号使用

在这里插入图片描述

kill——给指定进程发送指定信号

在这里插入图片描述
在这里插入图片描述

rase——给当前进程发送指定信号,abort——给当前进程发送终止信号

在这里插入图片描述

alarm函数——设定定时器

在这里插入图片描述
该函数时是不阻塞的
4. alarm测试
在这里插入图片描述
这里时把一秒内的数字都输出到终端,经过执行发现,终端其实超过了一秒才完全输出完数据
在这里插入图片描述
这里的原因时,往终端输出数据是需要花费挺多时间的,并且终端输出的缓冲区也是有限的
这里可以这样操作一下,不直接输出到终端,让他重定向输出的文件中
在这里插入图片描述
这样发现速度超级快,并且文件中数据远远比终端输出的要多,当然这样写文件,也是会花费一些时间的
在这里插入图片描述
程序执行时间其实是由这几部分组成的
在这里插入图片描述

setitimer——设置周期性的定时器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 具体设置步骤
    在这里插入图片描述

信号捕捉

在这里插入图片描述

  1. signal-函数
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. sigaction函数
    他和signal函数都是信号捕捉,这个的功能更多一些,signal是一个特殊标准,更推荐使用sigaction
    在这里插入图片描述

在这里插入图片描述

信号集

放在内核区中

在这里插入图片描述

在这里插入图片描述

信号集操作函数

  1. 这些函数只能操作自己定义的信号集(就是那个结构体)进行一些设置
  2. 在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

调用系统函数使用设置的信号集

  1. 上面提到的信号集操作,是这样的,定义一个信号集结构体之后(sigset_t set),用上面那些函数,可以对修改结构体里面的值,但是修改之后,他还是不能发挥作用的,要发挥作用,就需要调用系统函数,使设置好的信号集生效。
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

命令行的一些补充知识

  1. 中运行程序分为前台运行和后台运行
  2. 前台运行的时候,他就是会占用这个终端,此时你在终端输入 命令是没有用的
  3. 后台运行的时候,他会往终端打印信息,但是不会占用终端,你输入命令也是可以用的,通过在运行程序后面加个&号,他就是以后台方式运行
  4. 当程序在后台运行时,输入fg命令,就可以切换到前台

SIGCHLD信号

在这里插入图片描述

共享内存

在这里插入图片描述
在这里插入图片描述

  1. 他的步骤,先创建、再关联、再分离、最后再销毁

共享内存操作函数

在这里插入图片描述

  1. shmget——创建共享内存
    在这里插入图片描述
    在这里插入图片描述
    key参数那里,如果进程1是100,进程2也要设为100,这样他俩才能共享到同一块内存
  2. shmat——将当前进程与共享内存关联
    在这里插入图片描述
  3. shmdt——将共享内存与当前进程分离
    当一个进程结束时,会自动和共享内存分离
    在这里插入图片描述
    一个进程和一个共享内存分离后,就不能再操作这块内存了,也不能再次去关联
  4. shmctl——操作共享内存
    在这里插入图片描述
    设置了IPC_RMID之后,共享内存的key就会变成0,此时只是标记删除,要等所有的进程都解除关联之后,才会被真正释放。

共享内存注意事项

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用共享内存进行进程间通信

  1. 写进程:
    在这里插入图片描述

  2. 读进程
    这里读这端,他去获取的共享内存大小不能大于开辟的那边的大小
    3.

查看进程间通信方式和共享内存有关的命令

在这里插入图片描述

消息队列

在这里插入图片描述
在这里插入图片描述
在内核中会开辟出一块空间,是一个存放消息的队列(先进先出),本质是个链表,进程通过获取对应的消息队列id,往队列里面发送或者读取消息,

通过ipcs -q可以查看到目前有的消息队列
在这里插入图片描述
linux中有两种消息队列
在这里插入图片描述

System V

在这里插入图片描述

  1. msggget可以完成消息队列的创建,传入的第一个参数决定着这个消息队列的身份标识,两个进程要想访问同一个消息队列的话,这个值需要一样
  2. 这个值可以通过一个ftok()函数来创建
  3. 消息里面的mtype是消息类型,你发什么类型的消息,就要读什么类型的消息

发送消息
在这里插入图片描述
消息接收
在这里插入图片描述

Posix

在这里插入图片描述

发送消息
在这里插入图片描述

接收消息

在这里插入图片描述
在这里插入图片描述

守护进程

终端

在这里插入图片描述

进程组

在这里插入图片描述

会话

在这里插入图片描述

  1. 进程组的首进程不能创建新的会话

在这里插入图片描述
3. 开启一个终端后,就自动启动一个bash进程,在后台运行,他就会自动生成一个进程组,然后再生成一个会话,他的PID是400,PGID也是400,SID也是400
4. 然后启动了find和wc在后台运行,find的PID是658,那find就又生成一个进程组,进程组号PGID是658,wc和find同在一个进程组中,所以wc的PGID是658
5. 然后启动了sort和uniq在后台运行,sort的PID是660,那sort就又生成一个进程组,进程组号PGID是660,sort和uniq同在一个进程组中,所以uniq的PGID是660
6. 由于658、660和400都是会话400下面的进程组,所以这些里面的进程他们的会话SID都是400
7. 由于一个会话智能又一个前台进程组,所以660在前台,660里面的进程才可以使用终端输入输出,658和400都在后台

守护进程

在这里插入图片描述

守护进程的创建

在这里插入图片描述

  1. 要用子进程去创建新会话的原因:在调用setsid()的时候会直接创建一个新会话,会话的SID为该进程的PID,组PGID也是该进程的PID,如果用的是父进程来创建新会话,由于父进程PID=100是他所在进程组的首进程,进程组的PGID=100,当父进程调用setsid之后,创建的新会话中就会和目前的冲突,所以不能用父进程来创建新会话。
  2. 案例
/* 写一个守护进程,每隔2s获取一下系统时间,将这个时间写入到磁盘文件中。*/
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <string.h>

void work(int num) {
    // 捕捉到信号之后,获取系统时间,写入磁盘文件
    time_t tm = time(NULL);
    struct tm * loc = localtime(&tm);
    // char buf[1024];

    // sprintf(buf, "%d-%d-%d %d:%d:%d\n",loc->tm_year,loc->tm_mon
    // ,loc->tm_mday, loc->tm_hour, loc->tm_min, loc->tm_sec);

    // printf("%s\n", buf);

    char * str = asctime(loc);
    int fd = open("time.txt", O_RDWR | O_CREAT | O_APPEND, 0664);
    write(fd ,str, strlen(str));
    close(fd);
}

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

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

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

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

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

    // 6.业务逻辑
    // 捕捉定时信号
    struct sigaction act;
    act.sa_flags = 0;
    act.sa_handler = work;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM, &act, NULL);

    struct itimerval val;
    val.it_value.tv_sec = 2;
    val.it_value.tv_usec = 0;
    val.it_interval.tv_sec = 2;
    val.it_interval.tv_usec = 0;

    // 创建定时器
    setitimer(ITIMER_REAL, &val, NULL);

    // 不让进程结束
    while(1)
        sleep(10);
 
    return 0;
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值