使用fork创建多进程
首先对fork函数进行简单介绍:
fork()函数不同于普通函数,主要体现在其返回值上,返回值
- 小于0:fork()执行失败
- 等于0:子进程, 在子进程中fork返回的是0
- 大于0:父进程, 在父进程中fork返回的是创建的子进程的pid
用程序来详细解释一下:
pid_t ret; // fork 返回值类型
if ((ret = fork()) < 0)
{
fprintf(stderr,"fork fail. ErrNo[%d],ErrMsg[%s]\n",errno,strerror(errno));
}
else if (ret > 0)
{
// 该花括号内的所有语句属于父进程,父进程执行这些语句
fprintf(stdout,"** parent process run. pid:[%6d], ppid:[%6d],ret:[%6d]**\n",getpid(),getppid(),ret);
}
else
{
// 该花括号内的所有语句属于子进程,子进程执行这些语句
fprintf(stdout,"** child process run. pid:[%6d], ppid:[%6d],ret:[%6d] **\n",getpid(),getppid(),ret);
}
// 父子进程都会执行的一句
printf("========== last line. pid:[%6d], ppid:[%6d],ret:[%6d] ==========\n",getpid(),getppid(),ret);
对fork函数的解释:
1. fork函数理解为,在执行的瞬间将程序进行了拷贝,原本的可称为父进程,拷贝出来的是子进程。
2. 这种拷贝可理解为包括寄存器等所有内容的拷贝,自然也包括了程序计数器pc,如此子进程和父进程都从fork函数之后的地址继续往下执行。
3. 实质上,父子进程拥有这相同的代码,但是为了控制父进程执行父进程的代码,子进程执行子进程的。这是fork()函数在父子进程中不同的返回值就派上了用场。
4. 通过 if 判断,控制父进程执行父进程的执行体,子进程执行子进程的。也就是说父子进程的代码段是相同的。每个人都有一个副本,仅仅是执行了自己要执行的部分,对方的自己有却没有执行。
5. 再说程序的数据段,同样也是各自有着一个副本,变量的名称和结构相同,但是当父进程对其进行修改的话,也只有父进程可见,子进程不可见。原因就在于子进程有着自己的副本,自己没有改,就不会有变化。
通过下面的程序可以验证:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#define SHM_SIZE 4096
int main()
{
char buf[30];
memset(buf, 0x00, 30);
memcpy(buf, "default value", 13);
pid_t ret;
if ((ret = fork()) < 0)
{
printf("fork fail. ErrNo[%d],ErrMsg[%s]\n",errno,strerror(errno));
}
else if (ret > 0) // 父进程
{
// 读写操作
memset(buf, 0x00, 30);
memcpy(buf, "hello world! can you read me?", 29);
// 验证写入正常
printf("parent read SHM [%s]\n",buf);
// 写完等5秒,让子进程读取,否则程序就结束了,也就执行最后的删除共享内存操作了
sleep(5);
}
else // 子进程
{
printf("child sleep(3), wait for parent to write\n");
sleep(3);
// 读写操作
printf("child read SHM [%s]\n",buf);
}
// 该句父子进程都会执行,因为没有 if 判断返回值,父子进程都可就的同时也都执行了它
printf("pid = [%5d] ppid = [%5d] ret = [%5d], last line read SHM [%s]\n",getpid(),getppid(),ret,buf);
return 0;
}
执行结果为:
parent read SHM [hello world! can you read me?]
child sleep(3), wait for parent to write
child read SHM [default value]
pid = [16922] ppid = [16921] ret = [ 0], last line read SHM [default value]
pid = [16921] ppid = [10783] ret = [16922], last line read SHM [hello world! can you read me?]
- ret = 0 是子进程的执行结果,它感知不到父进程的修改
- ret != 0 是父进程,他可以看到自己的修改
- 总之,两两不相知!
响应召唤,进程间通信就横空出世了
为了让进程间可以共享数据,方法甚多,比如:通过文件,数据库等,典型的进程间通(IPC)信应当是:消息队列、信号量和共享内存。
这里我说说共享内存,其余用到了在补充,看程序如下(类似与上面的例子):
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#define SHM_SIZE 4096
int main()
{
// 1.创建共享内存,获取共享内存的标识符
int shmid; //共享内存标识符
shmid_ds * buf; //共享存储段基本信息结构体
buf = NULL;
shmid = shmget(IPC_PRIVATE, SHM_SIZE, 0666|O_CREAT);
pid_t ret;
if ((ret = fork()) < 0)
{
printf("fork fail. ErrNo[%d],ErrMsg[%s]\n",errno,strerror(errno));
}
else if (ret > 0) // 父进程
{
void *parent_shm = NULL; // 共享内存的首地址
// 2.连接共享内存,第二个0代表由内核选择可用地址,是推荐用法
parent_shm = shmat(shmid, 0, 0);
if ((void*)-1 == parent_shm)
{
printf("parent shmat fail. ErrNo[%d],ErrMsg[%s]\n",errno, strerror(errno));
}
// 读写操作
memcpy((char*)parent_shm, "hello world! can you read me?", 29);
// 3.断开共享内存
if (-1 == shmdt(parent_shm))
{
printf("parent shmdt fail. ErrNo[%d],ErrMsg[%s]\n",errno, strerror(errno));
}
// 写完等5秒,让子进程读取,否则程序就结束了,也就执行最后的删除共享内存操作了
sleep(5);
}
else // 子进程
{
printf("child sleep(3), wait for parent to write\n");
sleep(3);
void *child_shm = NULL; // 共享内存的首地址
// 2.连接共享内存,第二个0代表由内核选择可用地址,是推荐用法
child_shm = shmat(shmid, 0, SHM_RDONLY);
if ((void*)-1 == child_shm)
{
printf("child shmat fail. ErrNo[%d],ErrMsg[%s]\n",errno, strerror(errno));
}
// 读写操作
printf("child read SHM [%s]\n",(char*)child_shm);
// 3.断开共享内存
if (-1 == shmdt(child_shm))
{
printf("child shmdt fail. ErrNo[%d],ErrMsg[%s]\n",errno, strerror(errno));
}
exit(0); // 这是一个控制让子进程不执行后面代码的方法,结束掉他
}
// 此处没有了上面的程序中的输出语句,首先子进程已经执行不到这里了,其次,无论父子进程执行到这里都读取不到数据,因为他们都调用了shmdt断开了和共享内存的连接。如果没有断开,当然是可读的。同时还需要把parent_shm与child_shm的指针声明放到全局main函数区
// 4.删除共享内存
if (shmctl(shmid, IPC_RMID, buf) != 0)
{
printf("main shmctl IPC_RMID fail. ErrNo[%d],ErrMsg[%s]\n",errno, strerror(errno));
}
}
执行结果:
child sleep(3), wait for parent to write
child read SHM [hello world! can you read me?]
总结:子进程读到父进程所写的数据了。
共享内存函数总结:
① 获取共享内存表示符
- int shmget(ket_t key, size_t size, int flag);
② 连接共享内存
- void *shmat(int shmid, const void *addr, int flag);
③ 断开共享内存的连接
- int shmdt(const void *addr);
④ 共享内存的控制, 包括修改读写权限, 删除等
- int shmctl(int shmid, int cmd, struct shmid_ds *buf);
总结:共享内存的使用还是挺简单的,只需要知道这四个函数,以及他们的返回值和参数就够了,具体函数解释可以man一下,或者《UNIX环境高级编程》,我就不做搬运工了。
没错,这样是有问题的,进程间通信不提供同步机制 —- 信号量 出场
上面的方法通过使用sleep()简单进行了进程间同步,这在真是环境中是不可用的,那么这里就需要 信号量 来进行同步了(同步,就是说一个进程在写的时候另一进程不能去读或者写,否则最总出来的结果有问题,数据不正确,不可用)。
概念:信号量本身是一个计数器,用于为多个进程提供对共享数据对象的访问。所以它与管道,消息队列,共享内存本质上是有区别的(一个是控制访问的,其余是进行数据共享的)。
看程序:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <semaphore.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <fcntl.h>
using namespace std;
volatile int flag = 1;
const char SEM_MUTEX[10] = "sem_mutex";
int main()
{
sem_t *sem_t_mutex;
// 1.创建信号量,初始值为1
sem_t_mutex = sem_open(SEM_MUTEX, O_CREAT, 0777, 1);
if (SEM_FAILED == sem_t_mutex) {
printf("mutex 创建失败");
}
pid_t ret;
if ( (ret=fork()) < 0 )
{
fprintf(stderr,"fork fail. ErrNo[%d],ErrMsg[%s]\n",errno,strerror(errno));
}
else if ( 0==ret ) // 子进程
{
printf("clild create ok\n");
// 2. 加锁,实质上就是给信号量做减一操作,若本身已经为0,程序阻塞到这里,等待
if (sem_wait(sem_t_mutex)!=0) // 加锁
printf("lock fail\n");
int i = 0;
while(i < 4)
{
i++;
printf("clild: run in while\n");
sleep(1);
}
// 3.解锁,实质上就是给信号量做加一操作,加一后,那些等待的,处于阻塞的进程就能运行了
if (sem_post(sem_t_mutex)!=0)
printf("unlock fail\n");
exit(0); // 可以结束掉子进程,那么程序将不会再运行最后一行的printf,原因在于:fork之后的代码父子进程都可见都会执行,通过if可以控制父子进程进行执行不同的内容,原理在于fork不同于其他函数他返回两个值,给父进程返回的是子进程的pid,给子进程自己返回的是0,失败返回 -1,于是当在if中碰到exit自然子进程就结束了
}
else // 父进程
{
sleep(1);
printf("parent can run ok\n");
// 2. 加锁,实质上就是给信号量做减一操作,若本身已经为0,程序阻塞到这里,等待
if (sem_wait(sem_t_mutex)!=0)
printf("parent lock fai\n");
int j = 0;
while(j<10)
{
j++;
fprintf(stdout,"** parent process run. pid:[%6d], ppid:[%6d],ret:[%6d] **\n",getpid(),getppid(),ret);
sleep(1);
}
// 3.解锁,实质上就是给信号量做加一操作,加一后,那些等待的,处于阻塞的进程就能运行了
sem_post(sem_t_mutex);
}
// 4. 关闭,若没有该步骤,程序结束就会执行关闭
sem_close(sem_t_mutex);
// 5. 销毁,** 特别重要 ** ,不销毁,信号量的值会一直保留,比如信号量在退出时是 1 ,下次执行依旧会存在,导致二次运行程序的时候出错。
sem_unlink(SEM_MUTEX);
printf("========== last line. pid:[%6d], ppid:[%6d],ret:[%6d] ==========\n",getpid(),getppid(),ret);
}
运行结果:
clild create ok
clild: run in while
parent can run ok
clild: run in while
clild: run in while
clild: run in while
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
** parent process run. pid:[ 27104], ppid:[ 26251],ret:[ 27105] **
========== last line. pid:[ 27104], ppid:[ 26251],ret:[ 27105] ==========
结论:父进程sleep(1) 使得子进程获得了锁,于是父进程在sleep(1)后阻塞,知道子进程运行结束,释放锁,父进程才能执行,表现出来就是,子进程先执行,父进程后执行。如果没有锁,可以肯定,是乱序执行的,比如,注释掉加锁的相关行,执行结果如下:
clild create ok
clild: run in while
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
clild: run in while
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
clild: run in while
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
clild: run in while
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
** parent process run. pid:[ 27525], ppid:[ 26251],ret:[ 27526] **
========== last line. pid:[ 27525], ppid:[ 26251],ret:[ 27526] ==========
信号量这样用才好
程序如下:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <semaphore.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <fcntl.h>
using namespace std;
volatile int flag = 1;
const char SEM_MUTEX[10] = "sem_mutex";
/* 多进程的基本用法 */
int main()
{
sem_t *sem_t_mutex=NULL;
sem_t_mutex = sem_open(SEM_MUTEX, O_CREAT, 0777, 0);
if (SEM_FAILED == sem_t_mutex) {
printf("mutex 创建失败");
}
pid_t ret;
if ( (ret=fork()) < 0 )
{
fprintf(stderr,"fork fail. ErrNo[%d],ErrMsg[%s]\n",errno,strerror(errno));
}
else if ( 0==ret ) // 子进程
{
printf("clild create ok\n");
if (sem_wait(sem_t_mutex)!=0) // 加锁
printf("lock fail\n");
int i = 0;
while(i < 4)
{
i++;
printf("clild: run in while\n");
sleep(1);
}
exit(0); // 可以结束掉子进程,那么程序将不会再运行最后一行的printf,原因在于:fork之后的代码父子进程都可见都会执行,通过if可以控制父子进程进行执行不同的内容,原理在于fork不同于其他函数他返回两个值,给父进程返回的是子进程的pid,给子进程自己返回的是0,失败返回 -1,于是当在if中碰到exit自然子进程就结束了
}
else // 父进程
{
printf("parent can run ok\n");
int j = 0;
while(j<10)
{
j++;
fprintf(stdout,"** parent process run. pid:[%6d], ppid:[%6d],ret:[%6d] **\n",getpid(),getppid(),ret);
sleep(1);
}
// 只有等到父进程执行完了,释放了锁,子进程才能加锁成功,才能运行
if (sem_post(sem_t_mutex)!=0)
printf("unlock fail\n");
// 等子进程执行完了再运行最后那几行
sleep(5);
}
sem_close(sem_t_mutex);
sem_unlink(SEM_MUTEX);
printf("========== last line. pid:[%6d], ppid:[%6d],ret:[%6d] ==========\n",getpid(),getppid(),ret);
}
运行结果:
parent can run ok
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
clild create ok
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
** parent process run. pid:[ 28037], ppid:[ 26251],ret:[ 28038] **
clild: run in while
clild: run in while
clild: run in while
clild: run in while
========== last line. pid:[ 28037], ppid:[ 26251],ret:[ 28038] ==========
结论:
这里并没有为了让一个进程先运行,而让另一个sleep等待;而是采用了谁想先执行,就让对方直接去获取锁。注意,该处初始化锁的时候默认值是0,那么自然获取锁的时候就阻塞了。而先运行的进程在运行结束的时候释放锁,如此,当它运行完释放锁后,阻塞的进程就能执行了。
有了共享内存、信号量;这样就能二者结合正式使用了
这下其实就简单了,也不用多说了,也就不写了。
注:本文的目的就是直接教会怎么用共享内存