为什么需要多进程(多线程)
如果对于没有接触过多进程的朋友来说可能还不清楚多进程的作用和必要性,但是大家基本都会遇到程序响应不够,因为对于一个单进程程序来说,它的代码逻辑是一步步执行的,它在处理当前数据的时,并不能去做别的事,就像一个开车的司机,眼睛要看着路况,手要把握方向盘,而脚要控制油门和刹车,这些都是同时在动作的,我们如果希望自己的程序也能同时进行多个任务,就需要借助多进程(多线程)来完成.
一、什么是多进程?
在这之前我们要先了解,什么是进程和程序
1.进程和程序的概念
2.进程的状态
而且每个进程在运行的过程中都会处于不同的的状态,如图
那在一个程序中如果开了很多个进程,我们如何分辨它们呢?
其实每个进程都在创建时都有属于自己的pid号,同时pid也是控制进程的必须要素,我们通过函数getpid和getppid可以同时拿到,进程的pid和它父进程的pid,这两个函数的原型如下
pid_t getpid(void);,调用成功,返回进程的pid
pid_t getppid(void);调用成功,返回父进程的pid
3.进程相关的API
1)创建一个进程
创建进程我们需要用到fork函数:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);在当前进程创建一个子进程
无参数
成功:给调用的进程返回子进程的pid
给子进程返回0
失败:返回-1
pid_t pid = fork();
if(pid > 0)
{
//这里是父进程代码段
printf("父进程pid为%d\n",getpid());
printf("创建的子进程pid为%d\n",pid);
}
else if(!pid)
{
//这里是子进程代码段
printf("我是子进程,我的pid是%d\n",getpid());
}
else
{
//执行失败
perror("fock");
exit(1);
}
在fork函数调用时实际执行了这几步
- 给子进程也在内存中开辟了一块虚拟空间
- 复制父进程空间中的数据(代码段,静态变量,堆区,栈区)
- 给父进程返回子进程的pid,给子进程返回0
- fork返回后,父子进程同时从下一句代码开始执行
我们来看执行结果
2)给进程收尸
我们发现每次运行,父子进程的pid都不相同,而且有时候子进程的打印结果直接在命令行显示了,这是为什么呢?
之前说过,进程具有异步性,多个进程之间执行的时间是不一样的,这就导致了它们结束的时间也是未知的,而在上面的程序中,父进程需要打印两句话,而子进程只要打印一句话,所以正常情况下,父进程会比子进程先结束,而当父进程比子进程先结束时,就会出现刚刚错位打印的情况
创建进程是需要资源的,而一般来说,如果进程结束后没有主动释放资源,进程就会进入僵尸态,这是就需要父进程,或系统帮忙回收子进程的资源,这种操作就叫收尸,收尸有两种方式
方法1:给任意一个先结束的子进程收尸(一次只能给一个收尸,如要给多个进程收尸需要多次调用)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
参数:保存子进程结束的状态,一般为NULL;
成功:返回收尸子进程的pid
失败:返回0
方法1:给指定pid的子进程收尸(一次只能收尸一次)
pid_t waitpid(pid_t pid,int *wstatus,int options)
参数1:要收尸的子进程pid
参数2:保存子进程的结束状态
参数3:是否阻塞
成功:返回收尸子进程的pid
失败:返回-1
这样我们改进一下上面的代码,让父进程给子进程收尸
pid_t pid = fork();
if(pid > 0)
{
//这里是父进程代码段
//收尸
wait(NULL);
printf("父进程pid为%d\n",getpid());
printf("创建的子进程pid为%d\n",pid);
}
else if(!pid)
{
//这里是子进程代码段
sleep(3);//让子进程先暂停3秒
printf("我是子进程,我的pid是%d\n",getpid());
}
else
{
//执行失败
perror("fock");
exit(1);
}
执行结果是,先等待3秒,子进程打印后,父进程才会打印,同时也不会错位打印,在子进程未结束时,父进程也不会继续执行后面的代码,而是阻塞了,等wait返回后才会往下走
二、进程之间的通信
进程之间通信的方法大概有以下几种
- 无名管道
- 有名管道
- 信号
- 消息队列
- 共享内存
- 信号灯(信号量)
- 套接字(一般用于网络数据传输)
1.无名管道
首先要用无名管道进行通信时,必须满足两个进程之间要有亲缘关系,因为无名管道通信,需要用到文件描述符,而子进程可以从父进程那里继承管道的文件描述符,如果两个进程之间并无亲缘关系,则无法获得管道的文件描述符
无名管道的特点:
1.进程之间需要有亲缘关系
2.无名管道是单兵工作模式
3.无名管道的读端和写端都是固定的
1)无名管道
#include <unistd.h>
int pipe(int pipefd[2]);
参数:保存管道文件描述的数组
成功:返回0
失败:返回-1
管道创建之后我们就可以用read和write函数读取和写入数据了
用父进程写入数据,子进程接收数据
注意用因为管道是单兵工作模式,所以在用读端时最好要关闭写端的文件描述符,反之再用写端时最好要关闭读端的文件描述符
//创建一个管道
int fd[2];
if(pipe(fd) < 0)
{
perror("pipe");
exit(1);
}
int pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
else if(!pid)
{
//子进程读取读取数据
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
read(fd[0],buf,sizeof(buf));
printf("接收到的数据为:%s",buf);
}
}
else
{
//父进程写入数据
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
write(fd[1],buf,strlen(buf));
}
}
执行结果如下:
2.有名管道
有名管道是无名管道的升级版,它解决了只有亲缘关系的进程才能通信的缺陷,即任意进程都可以借助有名管道进行通信,而且有名管道两端都可以进行读写,改进了无名管道的单兵工作模式
功能:在当前路径下创建一个有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数1:创建的管道名
参数2:管道的权限0666(能创建的最大权限)
成功:返回0
失败:返回-1
我们创建两个进程
进程1:写入数据
//创建一个管道
if(mkfifo("fifo",0666) < 0)
{
perror("mkfifo");
exit(1);
}
//打开管道
int fp = open("fifo",O_RDWR);
if(fp == NULL)
{
perror("open");
exit(1);
}
//写入数据
char buf[100];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
write(fp,buf,strlen(buf));
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//关闭管道
close(fp);
进程2:从管道中读取数据
int fp = open("fifo",O_RDWR);
if(0 > fp)
{
perror("open");
exit(1);
}
char buf[100];
//读取数据
while(1)
{
bzero(buf,sizeof(buf));
read(fp,buf,sizeof(buf));
printf("%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
close(fp);
执行结果如下
而且我们会发现当前路径下多了一个叫fifo的文件,这个就是有名管道创建后的文件,我们可以让任意的进程去访问fifo
3.信号
信号是软件层对中断机制的一种模拟,是多进程之间的一种异步通信方式,内核进程可以通过信号来告诉用户当前系统发生的事件,我们也可以通过信号让进程处于暂停状态,先处理别的事情.
进程对信号的相响应方式有3种
- 忽略该信号
- 执行缺省操作(即默认处理方式)
- 捕捉信号(执行用户自定义的处理方式)
一些常见的信号
给一个进程发送信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
参数1:接收信号进程的pid
参数2:要发送的信号
成功:返回0
失败:返回-1
当我们想给一个进程发送信号时我们首先要知道它的pid号
注意事项
1.当一个进程未被执行的时候,给它发送的信号会被内核保存起来,直到该进程运行时,内核才会把信号发送给它
2.当进程被设置成阻塞状态时,收到的信号会延迟,直到阻塞完成时,才会收到信号
让一个进程挂起,直到收到任意信号才继续运行
#include <unistd.h>
int pause(void);
无参数
作用是让进程挂起,收到信号才继续执行
注册信号,自定义处理信号函数
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
参数1:要注册的信号
参数2:一个函数指针,收到注册信号后的处理函数,该函数要有一个int类型的形参用于接收信号
void fun(int sig)
{
…
}
这个函数可以让我们自定义当进程收到注册信号后,要执行或处理的事情
现在我写一段这样的代码:
父进程给子进程发送信号,子进程在收到信号之让它处于挂起状态,收到信号后打印自己的pid
void fun(int sig)
{
printf("收到信号\n");
printf("%d\n",getpid());
]
int main(void)
{
//创建一个子进程
pid_t pid = fork();
if(0 > pid)
{
perror("fork");
exit(1);
}
else if(!pid)
{
printf("子进程挂起中....\n");
//子进程注册信号
signal(SIGUSR1,fun);
pause();
exit(1);
}
else
{
//父进程倒计时发送信号
for(int i =0;i < 5;i++)
{
printf("倒计时发送信号%d....\n",i);
sleep(1);
}
//发送信号
kill(pid,SIGUSR1);
//收尸
wait(NULL);
printf("完成\n");
}
return 0;
}
执行结果如下:
当我们要给没有亲缘关系的进程发送信号时,我们又要直到对方的pid,就可以利用有名管道先给对方发送自己的pid,然后再用kill发送信号
4.消息队列
消息队列和共享内存、信号量都是IPC对象,消息队列有点像我们消息列表,它可以接收并存储不同类型的消息,所以进程在向消息队列发送和读取消息时都会通过消息类型来进行分辨.
创建/打开一个消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数1:key值,需要用ftok()用函数获取
参数2:消息队列的权限 IPC_CREAT|0666
成功:返回消息队列的id
失败:返回-1
如果消息队列不存在,就创建它,如果消息队列存在就打开它
给消息队列发送消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数1:消息队列的id
参数2:消息存放的指针—是一个结构体需要自己定义
struct msgbuf {
long mtype; // message type, must be > 0 第一个成员必须为消息类型,后面的成员自定义
char mtext[1]; //message data 消息的正文
};
参数3:消息的大小
参数4:标签
IPC_NOWAIT 消息没有发送完成函数也会立即返回。
0:直到发送完成函数才返回
成功:返回0
失败:返回-1
从消息队列中读取一条消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数1:消息队列的id
参数2:保存读取消息的指针
参数3:读取消息的大小
参数4:读取消息的类型
参数5:标签
IPC_NOWAIT 没有要读取的消息时函数会立即返回。
0:没有要接收的消息则阻塞,直到有要接收的类型的消息
成功:返回0
失败:返回-1
这里我让父进程往消息队列发送消息,子进程负责从消息队列读取消息
//定义消息队列的结构体
struct msgbuf
{
long msgtype;//消息类型
char text[100];//消息正文
};
int main(void)
{
//创建一个消息队列
key_t key = ftok("./",1);
if(0 > key)
{
perror("ftok");
exit(1);
}
int msgid = msgget(key,IPC_CREAT|0666);
if(0 > msgid)
{
perror("msgget");
exit(1);
}
//创建子进程
pid_t pid = fork();
if(0 > pid)
{
perror("fork");
exit(1);
}
else if(!pid)
{
//子进程
struct msgbuf buf;
buf.msgtype = 100;//设置消息类型
while(1)
{
bzero(buf.text,sizeof(buf.text));
msgrcv(msgid,&buf,sizeof(buf.text),buf.msgtype,0);
printf("读取的消息:%s",buf.text);
}
}
else
{
//父进程
struct msgbuf buf;
buf.msgtype = 100;//设置消息类型
while(1)
{
bzero(buf.text,sizeof(buf.text));
fgets(buf.text,sizeof(buf.text),stdin);
//发送消息
msgsnd(msgid,&buf,strlen(buf.text),0);
printf("发送成功\n");
}
}
return 0;
}
执行结果如下:
5.共享内存
为了方便多个不同的进程之间通信,内核特地在内存中保留了一块物理内存空间
优点:
作为共享内存,同时它也是效率最高的通信方式,因为不需要数据拷贝,大大的提高了读写的效率,进程要访问只需要把共享内存映射到自己的私有空间即可,
缺点:
也正是因为多个进程同时读写,可能会发生访问冲突,需要一些同步机制来预防冲突行为,例如互斥锁,信号灯等
创建/打开一块共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数1:key值
参数2:创建共享内存的大小
参数3:共享内存的权限 IPC_CREAT|0666
成功:返回共享内存的id
失败:返回-1
如果共享内存不存在,则创建并打开它,存在就打开它
把共享内存映射到当前进程
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数1:共享内存的id
参数2:映射到当前进程的空间地址,一般默认为NULL,让系统自动映射
参数3:共享内存的权限,SHM_RDONLY -只读,0 — 可读可写
成功:返回映射虚拟空间的地址
失败:返回0
解除映射
int shmdt(const void *shmaddr);
参数:映射共享内存的地址
成功:返回0
失败:返回-1
下面我们尝试在进程中创建一块共享内存,使用的时候记得用信号限制一下,防止父子进程冲突
#define SIZE 200
void fun(int sig)
{
printf("read:\n");
}
int main(void)
{
//申请一块共享内存
key_t key = ftok("./",1);
if(0 > key)
{
perror("ftok");
exit(1);
}
shmid = shmget(key,SIZE,IPC_CREAT|0666);
if(0 > shmid)
{
perror("shmid");
exit(1);
}
//创建一个子进程
pid_t pid = fork();
if(0 > pid)
{
perror("fork");
exit(1);
}
else if(!pid)
{
//子进程
//映射共享内存到子进程
char *buf = shmat(shmid,NULL,0);
//注册信号
signal(SIGUSR1,fun);
//打印共享空间的数据
while(1)
{
//挂起进程,等待父进程写完后发送信号
pause(;)
printf("数据为:%s",buf);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//解除映射
shmdt(buf);
exit(1);
}
else
{
//父进程
//映射共享内存到父进程
char *buf = (char *)shmat(shmid,NULL,0);
//往共享内存中写入数据
while(1)
{
fgets(buf,SIZE,stdin);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
kill(pid,SIGUSR1);
}
//解除映射
shmdt(buf);
}
return 0;
}
6.信号量/信号灯
信号是一种用于进程之间的同步机制,一般分为二值信号(信号为0和1,表示资源是否可用)和计数信号(信号为0和n,表示资源还剩多少)
信号中的p/v操作
创建信号
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数1:key值
参数2:要创建信号量的个数
参数3:信号的权限 IPC_CREAT|0666
成功:返回信号量对象的id
失败:返回-1
实现P/V操作
int semop(int semid, struct sembuf *sops, size_t nsops);
参数1:信号id
参数2:信号结构体
struct sembuf
{
unsigned short sem_num //被操作信号量的下标
short sem_op// P操作:-1 V操作:1
short sem_flg// 信号标签
}
参数3:要操作的信号量的个数
成功:返回0
失败:返回-1
struct sembuf
{
unsigned short sem_num //被操作信号量的下标
short sem_op// P操作:-1 V操作:1
short sem_flg// 信号标签
};
//P操作:
void sem_p(int semid,int index)//1.信号的id 2.被操作的信号下标
{
struct sembuf sem{
.sem_num = index;
.sem_op = -1;
.sem_flg = 0;
}
//V操作:
void sem_v(int semid,int index)//1.信号的id 2.被操作的信号下标
{
struct sembuf sem{
.sem_num = index;
.sem_op = 1;
.sem_flg = 0;
}
控制信号的
int semctl(int semid, int semnum, int cmd, …);
参数1:信号id
参数2:操作的信号个数
参数3:选项
GETVAL:获取某个信号灯的值
SETVAL:设置某个信号灯的值
SETALL: 设置多个信号灯的值
GETALL:获取多个信号灯的值
IPC_RMID:从系统中删除信号灯集合
参数3:变参是一个联合体,需要用户自己定义,当参数2为删除信号时,一般传NULL
union semun {
int val; //给单个信号赋值
struct semid_ds *buf; //使用缓存区
unsigned short *array; //用数组给一组信号赋值
struct seminfo *__buf;//使用缓存区
}
一般该函数多用于初始化
union semun
{
int val;
unsigned short *array;
};
//初始化单个信号
void sem_init(int semid,int index,int val)
{
union semun buf;
buf.val = val;
if(semctl(semid,index,buf) < 0)
{
perror("semctl");
exit(1);
}
}
//初始化多个信号
void sem_init(int semid,int index,unsigned short *array)
{
union semun buf;
buf.array = array;
if(semctl(semid,index,buf) < 0)
{
perror("semctl");
exit(1);
}
}
信号量一般会配合共享内存使用,实现多个进程之间的同步