Linux应用编程(四):进程间通讯

一:进程介绍

1、进程概念

       进程是具有一定独立功能的程序在一个数据集合上的一次动态执行过程。它是动态的,包括创建、调度、执行和消亡(由操作系统完成的)。

2、进程的操作

(1)创建进程

头文件: #include 

函数原型: pid_t fork(void);

返回值:  成功时,父进程返回子进程的进程号(>0的非零整数),子进程中返回0;通过fork函数的返回值区分父子进程。

父进程: 执行fork函数的进程。

子进程: 父进程调用fork函数之后,生成的新进程。

进程的特点:

  • 在linux中,一个进程必须有父进程,但是可以没有子进程
  • 子进程继承了父进程的内容,包括父进程的代码,变量,甚至包括当前的PC值,父子进程的执行顺序是不确定的,取决于当前系统的调度
  • 父子进程有独立的地址空间,独立的代码空间,互不影响。
  • 子进程结束后必须由它的父进程收回它的一切资源,否则就会成为僵尸进程
  • 如果父进程先结束,子进程会变成孤儿进程,它会被INIT进程收养,INIT进程是内核启动后首先被创建的进程。

(2)结束进程

使用exit函数来结束一个进程
头文件: #include 
函数原型: void exit (int status);

使用_exit函数来结束一个进程
头文件: #include 
函数原型: void _exit(int status);


区别:exit结束进程时会刷新缓冲区,_exit不会

(3)回收进程

使用wait函数来回收一个进程
头文件: #include 
	   #include 
函数原型: pid_t wait(int *status);
返回值:  成功返回子进程的进程号,失败返回-1

使用waitpid函数来回收一个进程
头文件: #include 
	   #include 
函数原型: pid_t waitpid(pid_t pid, int *status, int options);
返回值:  成功返回子进程的进程号,失败返回-1

二:进程间通信

        Linux中,内存空间被划分为用户空间和内核空间,应用程序存在于用户空间,绝大部分进程都处于用户空间;驱动程序存在于内核空间,在用户空间,不同进程不能互相访问对方的资源,因此,在用户空间是无法实现进程间通信的,为了实现进程间通信,必须在内核空间,由内核提供相应的接口来实现,在linux中一切设备皆文件,内核中实现进程间通信也是基于文件读写的方式。不同进程通过操作内核中同一个对象来实现进程间通信。Linux系统提供了4种进程间通信方式,

Linux应用编程(四):进程间通讯_第1张图片

1、管道通信

(1)无名管道

特点:在文件系统中没有文件节点,只能用于具有亲缘关系的进程间通信(比如父子进程),无名管道实际上就是一个单向队列,在一端进行读操作,在另一端进行写操作,所以需要两个文件描述符,fd[0]指向读,fd[1]指向写。它是一个特殊的文件,所以无法使用open函数来创建,而是需要使用pipe函数来创建。

a.创建无名管道

1.头文件#include 
2.函数原型: int pipe(int fd[2])
3.参数: 管道文件描述符,有两个文件描述符,分别是fd[0]和fd[1],管道有一个读端
4.      fd[0]和一个写端fd[1]
5.返回值: 0表示成功;1表示失败

b.读,写,关闭管道

1.读管道 read,读管道对应的文件描述符是fd[0]
2.写管道 write,写管道对应的文件描述符是fd[1]
3.关闭管道 close,因为创建管道时,会同时创建两个管道文件描述符,分别是读管道文件描述符fd[0]和写管道文件描述符fd[1],因此需要关闭两个文件描述符

c.示例

#include 
#include 
#include 
#include 
#include 
#include 

int
main(int argc, char *argv[])
{
    int pipefd[2];
    pid_t cpid;
    char buf;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s \n", argv[0]);
        exit(EXIT_FAILURE);
    }

    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    cpid = fork();
    if (cpid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (cpid == 0) {    /* Child reads from pipe */
        close(pipefd[1]);          /* Close unused write end */

        while (read(pipefd[0], &buf, 1) > 0)
            write(STDOUT_FILENO, &buf, 1);

        write(STDOUT_FILENO, "\n", 1);
        close(pipefd[0]);
        _exit(EXIT_SUCCESS);

    } else {            /* Parent writes argv[1] to pipe */
        close(pipefd[0]);          /* Close unused read end */
        write(pipefd[1], argv[1], strlen(argv[1]));
        close(pipefd[1]);          /* Reader will see EOF */
        wait(NULL);                /* Wait for child */
        exit(EXIT_SUCCESS);
    }
}

(2)有名管道

特点:在文件系统中存在文件节点,适用于同一系统中的任意两个进程间通信

a.创建有名管道

函数原型 : int mkfifo(const char * filename, mode_t mode)
参数 :管道文件文件名,权限,创建的文件权限仍然和umask有关系
返回值 : 成功返回0,失败返回-1

注意:mkfifo并没有在内核中生成一个管道,只是在用户空间生成了一个有名管道文件

b.示例

#include 
#include 
#include 
#include 
#include 

/*   进程一 */
int main(int argc, char *argv[])
{
    int i, ret, fd;
    char p_flag = 0;

    /* 创建有名管道 */
    if (access("./3rd_fifo", 0) < 0) {   //先判断有名管道文件是否存在,不存在需要先创建
        ret = mkfifo("./3rd_fifo", 0777);
        if (ret < 0) {
            printf("create named pipe fail\n");
            return -1;
        }
        printf("create named pipe sucess\n");
    }   
    /* 打开有名管道,以写方式打开 */
    fd=open("./3rd_fifo", O_WRONLY);
    if (fd < 0) {
        printf("open 3rd_fifo fail\n");
        return -1;
    }
    printf("open 3rd_fifo sucess\n");
    for (i = 0; i < 5; i++) {
        printf("this is first process i=%d\n", i);
        usleep(100);
    }
    p_flag = 1;
    sleep(5);
    write(fd, &p_flag, sizeof(p_flag));
    while(1);
    return 0;
}


/*   进程二  */
int main(int argc, char *argv[])
{
    int i;
    int fd=open("./3rd_fifo", O_RDONLY);
    char p_flag = 0;
    
    if (fd < 0) {
        printf("open 3rd_fifo fail\n");
        return -1;
    }   
    printf("open 3rd_fifo sucess\n");
    read(fd, &p_flag, sizeof(p_flag));
    while(!p_flag);
    for (i = 0; i < 5; i++) {
        printf("this is second process i=%d\n", i);
        usleep(100);
    }
    while(1);
    return 0;
}

2、IPC通信

IPC通信分为共享内存,消息队列以及信号灯,这些IPC对象都存在于内核空间中。

应用程序使用IPC通信的一般步骤为:

  • 首先生成一个key值。有两种生成key的方式,一种是使用宏IPC_PRIVATE表示一个key,它表示一个私有对象,只能用于当前进程或者具有亲缘关系的进程访问。另一种是使用ftok函数来生成一个key值,这种方式创建的IPC对象可以被不同的进程访问。
  • 使用生成的key值,创建一个IPC对象(如果是已经创建好的IPC对象,则打开该IPC对象),这个时候每个IPC对象都有一个唯一的ID号(IPC_id,可以是shm_id,msg_id,sem_id,每个id代表一个IPC对象)。
  • 进程通过IPC_id,调用访问IPC通道的读写函数来操作IPC对象。调用shmctrl,shmat,shmdt来访问共享内存;调用msgctrl,msgsnd,msgrecv访问消息队列;调用semctrl,semop访问信号灯。
函数原型 : char ftok(const char *path, char key)
参数 : path,存在并且可以访问的文件路径
        key,一个字符
返回值 : 正确返回一个key值,出错返回-1

(1)共享内存

       共享内存是指多个进程都可以访问的一块地址空间,由于Linux内核保证数据安全的决策,为不同进程划分了独自的地址空间,每个进程不能访问其他进程的地址空间,因此为了解决这个问题,内核开辟了一块物理内存区域,进程本身可以将这块内存空间映射到自己的地址空间进行读写。进程可以直接访问这片内存,数据不需要在两个进程之间复制,所以速度非常快,共享内存没有任何同步与互斥机制,所以要使用信号量来对共享内存的存取进行同步。

Linux应用编程(四):进程间通讯_第2张图片

共享内存通信步骤:

  • 先创建一片共享内存,该内存存在于内核空间中。
  • 进程通过key值找到这片共享内存的唯一ID,然后将这片共享内存映射到自己的地址空间。
  • 每个进程通过读写映射后的地址,来访问内核中的共享内存。

a.创建共享内存

函数原型 : int shmget(key_t key, int size, int shmflg)
头文件: #include 
函数参数 : key: IPC_PRIVATE 或 ftok的返回值
                IPC_PRIVATE返回的key值都是一样的,都是0
           size : 共享内存区大小
           shmflg : 同open函数的权限位,也可以用八进制表示法
返回值   : 成功,共享内存段标识符ID; -1 出错
/*  IPC_PRIVATE  方式   */
int shmid;
shmid = shmget(IPC_PRIVATE, 128, 0777);
if (shmid < 0) {
    printf("create shared memory fail\n");
    return -1;
}
printf("create shared memory sucess, shmid = %d\n", shmid);
system("ipcs -m");


/*   ftok 方式  */
int shmid;
int key;
key = ftok("./a.c", 'a');  //先创建一个key值
if (key < 0) {
    printf("create key fail\n");
    return -1;
}

printf("create key sucess key = 0x%X\n",key);
shmid = shmget(key, 128, IPC_CREAT | 0777);
if (shmid < 0) {
    printf("create shared memory fail\n");
    return -1;
}

b.应用程序访问共享内存

将共享内存映射到用户空间,这样应用程序就可以直接访问共享内存了
函数原型 : void *shmat(int shmid, const void *shmaddr, int shmflg)
参数 : shmid ID号
        shmaddr 映射地址, NULL为系统自动完成的映射
        shmflg SHM_RDONLY共享内存只读
               默认是0,可读可写
返回值:成功,映射后的地址;失败,返回NULL
int shmid;
char *p;

p = (char *)shmat(shmid, NULL, 0);
if (p == NULL) {
    printf("shmat fail\n");
    return -1;
}
printf("shmat sucess\n");

//等待console输入,然后向共享内存写入数据
fgets(p, 128, stdin);
//读共享内存
printf("share memory data:%s\n", p);

c.解除地址映射

函数原型:int shmdt(const void *shmaddr)
参数 ; shmat的返回值
返回值 : 成功0,出错-1

调用shmdt解除地址映射,此时应用程序继续访问会出错

shmdt(p);

d.共享内存控制指令函数

函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数 ; shmid : 共享内存标识符
        cmd : IPC_START (获取对象属性)      --- 实现了命令 ipcs -m
              IPC_SET (设置对象属性)
              IPC_RMID (删除对象属性)       --- 实现了命令 ipcrm -m
        buf : 指定IPC_START/IPC_SET时用以保存/设置属性
返回值 : 成功0,出错-1

删除共享内存
shmctl(shmid, IPC_RMID, NULL);

(2)消息队列

消息队列是消息的链表,它是一个链式队列,和管道类似,每个消息都有多大长度限制。可用cat/proc/sys/kernel/msgmax查看。

  • 生命周期跟随内核,消息队列一直存在,需要用户显示调用接口删除或者使用命令删除。
  • 消息队列可以实现双向通信
  • 克服了管道只能承载无格式字节流的缺点

a.创建消息队列

头文件:#include 
        #include 
       #include 
原型: int msgget(key_t key, int flag)
参数: key 和消息队列关联的key值
       flag 消息队列的访问权限
返回值: 成功,消息队列ID,出错 -1
int msgid, key;
struct msgbuf readbuf;

key = ftok("./a.c", 'a');
if (key < 0){
    printf("create key fail\n");
    return -1;
}
msgid = msgget(key, IPC_CREAT|0777);

b.消息队列控制函数

原型: int msgctl(int msgqid, int cmd, struct msqid_ds *buf)
参数: msgqid 消息队列ID
           cmd IPC_STAT 读取消息队列的属性,并将其保存在buf指向的缓冲区中
               IPC_SET  设置消息队列的属性,这个值取自buf参数
               IPC_RMID 从系统中删除消息队列
           buf 消息缓冲区
返回值: 成功 0,出错 -1

c.添加信息到消息队列

头文件#include 
      #include 
      #include 
原型: int msgsnd(int msgqid, const void *msgp, size_t size, int flag)
参数: msgqid 消息队列ID
           msgp 指向消息的指针,常用消息结构msgbuf如下
           struct msgbuf {
               long mtype;        //消息类型
               char mtext[N];     //消息正文
           };
           size 消息正文的字节数
           flag IPC_NOWAIT 消息没有发送完成也会立即返回
                0: 直到发送完成函数才会返回
    返回值: 成功 0,出错 -1
struct msgbuf {
    long type;        //消息类型
    char voltage[124];     //消息正文
    char ID[4];
};
struct msgbuf sendbuf;
msgsnd(msgid, (void *)&sendbuf, strlen(sendbuf.voltage), 0);

d.从消息队列中接收信息

原型: int msgrcv(int msgqid, void *msgp, size_t size, long msgtype, int flag)
参数: msgqid 消息队列ID
       msgp 接收消息的缓冲区
       size 要接收消息的字节数
       msgtype  0 接收消息队列中第一个消息
                大于0 接收消息队列中第一个类型为msgtype的消息
                小于0 接收消息队列中类型值不大于msgtype的绝对值且类型值又最小的消息
           flag IPC_NOWAIT 没有消息,会立即返回
                0: 若无消息则会一直阻塞
                
返回值: 成功 接收消息的长度,出错 -1
struct msgbuf {
    long type;        //消息类型
    char voltage[124];     //消息正文
    char ID[4];
};
struct msgbuf readbuf;
msgrcv(msgid, (void *)&readbuf, 124, 100, 0);

(3)信号灯

不同进程对于共享内存的访问是不确定的,共享资源一次只能允许一个进程访问,因此进程在访问共享资源时需要加上同步,互斥操作。

P操作:申请共享资源 

V操作:释放共享资源

信号量灯是信号量的集合,包含了多个信号量,可对多个信号灯同时j进行P/V操作,主要用于实现进程,线程间同步/互斥。

a.创建或打开函数

头文件#includde 
#includde 
#includde 
原型: int semget(key_t key, int nsems, int semflag)
参数: key 和信号灯集关联的key值
           nsems 信号灯集包含的信号灯数目
           semflag 信号灯集的访问权限
返回值: 成功,信号灯ID,出错 -1
shmid = shmget(key, N, IPC_CREAT|0666);
if (shmid < 0) {
	perror("shmid");
	exit(-1);
}

b.信号量灯控制函数

头文件#includde 
#includde 
#includde 
原型: int semctl(int semid, int semnum, int cmd, ...union semun arg)
          注意最后一个参数不是地址,可以有,可以没有
参数: semid 信号灯集id
       semnum 要修改的信号灯集编号,删除操作时,这个值可以设置为任意值
       cmd GETVAL 获取信号灯的值
           SETVAL 设置信号灯的值
           IPC_RMID 删除信号灯
       union semun arg: union semun {
                     int              val;    /* Value for SETVAL */
                     struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
                     unsigned short  *array;  /* Array for GETALL, SETALL */
                     struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
                 };
返回值: 成功,消息队列ID,出错 -1
semctl(semid, 0, IPC_RMID);

union semun myun;
myun.val = s[i];
semctl(semid, i, SETVAL, myun);

c.p/v操作函数

头文件 #includde 
#includde 
#includde 
原型: int semop(int semid, struct sembuf *opsptr, size_t nops)
参数: semid 信号灯集id
       opsptr struct sembuf{
                  short sem_num;   //要操作信号灯的编号
                  short sem_op;    //0: 等待,直到信号灯的值变为0,1:资源释放,V操作,-1:分配资源,P操作
                  short sem_flg;   //0: IPC_NOWAIT, SEM_UNDO
              }
       nops 要操作信号灯个数
返回值: 成功,消息队列ID,出错 -1
while (1) {
	pv(semid, WRITE, -1);
	printf("input > ");
	fgets(shmaddr, N, stdin);
	if (strcmp(shmaddr, "quit\n") == 0) break;
		pv(semid, READ, 1);
	}
	kill(pid, SIGUSR2);
}

3、信号通信

信号是软件层面上对中断机制的一种模拟,是一种异步通信方式。Linux内核通过信号通知用户进程,不同的信号类型代表了不同的事件。

常见的信号类型:

Linux应用编程(四):进程间通讯_第3张图片

a.kill函数发射信号给进程或进程组,raise函数则允许进程向自身发送信号

头文件	#include 
函数原型int kill(pid_t pid, int sig);
        int raise(int sig);
参数    pid : 指定接收进程的进程号
0代表同组进程;-1代表所有除了INIT进程和当前进程之外的进程
		sig : 信号类型
返回值  成功返回0,失败返回EOF


调用 
raise(sig);
等价于调用
kill(getpid(), sig);

b.alarm函数可以设置一个定时器,在将来的某个时刻定时器会超时,当超时时会产生SIGALRM信号

头文件	#include 
函数原型 int alarm(unsigned int seconds);
参数    seconds 定时器的时间
返回值  成功返回上个定时器的剩余时间,失败返回EOF

c.pause函数是调用进程挂起直到捕捉到一个信号

头文件	#include 
函数原型 int pause(void);
返回值   成功返回0,失败返回EOF

d.设置信号响应方式

头文件	#include 
函数原型 void (*signal(int signo, void(*handler)(int)))(int)
参数    signo 要设置的信号类型
		handler 指定的信号处理函数;
返回值  成功返回0,失败返回EOF
void myfun(int signum)
{
    return;
}
signal(SIGUSR1, myfun);

4、socket通信

socket后续开设专题讲解。

你可能感兴趣的:(Linux应用开发)