Ⅰ. wait和waitpid函数

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。

父进程可以调用waitwaitpid获取这些信息,然后彻底清除掉这个进程。

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

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

例如:一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shel1调用waitwaitpid得到它的退出状态,同时彻底清除掉这个进程。

如果一个进程已经终止,但是它的父进程尚未调用waitwaitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。可以用ps u查看进程状态。如果这时使用kill -9去杀该僵尸进程是杀不掉的。

父进程调用wait或waitpid时可能会:
1. 阻塞(如果它的所有子进程都还在运行)。
2. 带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
3. 出错立即返回(如果它没有任何子进程)。

这两个函数的区别是:
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在option参数中指定WNOHANG可以使父进程不阻塞而立即返回0。wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。

/**
 * @brief 演示僵尸进程的产生
 * 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。
 * 如果父进程先退出 ,子进程会成为孤儿进程,被init(pid == 1)接管,子进程退出后init会回收其占用的相关资源
*
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(-1);
    }
    if (pid) {
        // 父进程
        while (1) {
            sleep(1);
        }
    } else {
        // 子进程
        exit(-2);  // 成为僵尸进程,父进程在休眠没能去回收,如果使用kill -9去杀该子进程是杀不掉的
    }

    return 0;
}
// 如果使用Ctrl+C杀死主进程,那么bash会去收主进程的尸体,子进程会成为孤儿僵尸进程,被1号进程接
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 使用waitpid获取子进程的状态信息(在子进程的状态发生变化时获取)
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(-1);
    }

    if (!pid) {
        // 子进程
        int n = 10;
        while (n) {
            printf("this is child process %d\n", getpid());
            sleep(1);
            n--;
        }
        exit(-2);
    } else {
        // 父进程
        int stat_val;
        waitpid(pid, &stat_val, 0);  //pid就是子进程的pid
        if (WIFEXITED(stat_val)) {
            // 进程正常结束
            printf("child exited normally with code %d\n", WEXITSTATUS(stat_val));
        } else if (WIFSIGNALED(stat_val)) {
            // 进程被信号终止
            printf("child terminated abnormally by signal %d\n", WSTOPSIG(stat_val));
        }
    }

    return 0;
}

Ⅱ. 进程间通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC, InterProcess Communication)

Linux系统编程笔记(7)——进程体系与进程管理(2)-萤火

1. 管道(pipe)

管道是一种最基本的IPC机制,由pipe函数创建

#include <unistd.h>

int pipe(int pipefd[2]);

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过pipefd参数传出给用户程序两个文件描述符, pipefd[0]指向管道的读端,pipefd[1]指向管道的写端(就像0是标准输入,1是标准输出一样)

  1. 返回值:成功返回0,出错返回-1并设置errno
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 1.父进程创建管道
 * 	  2.父进程fork出子进程
 * 	  3.父进程关闭fd[0],子进程关闭fd[1],建立起管道通信
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    pid_t pid;
    int fd[2];
    if (pipe(fd) < 0) {
        perror("pipe\n");
        exit(-1);
    }
    pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-2);
    }
    if (pid) {
        // 父进程
        close(fd[0]);                       // 关闭读端
        write(fd[1], "hello world\n", 12);  // 往管道写数据
        wait(NULL);                         // 等待子进程结束
        close(fd[1]);
    } else {
        // 子进程
        close(fd[1]);  //关闭写端
        char buff[20];
        int n = read(fd[0], buff, 20);  //从管道中读数据
        write(1, buff, n);              //往标准输出中写数据
        close(fd[0]);
    }

    return 0;
}
Linux系统编程笔记(7)——进程体系与进程管理(2)-萤火
Linux系统编程笔记(7)——进程体系与进程管理(2)-萤火
Linux系统编程笔记(7)——进程体系与进程管理(2)-萤火

上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信。总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们能通信。

使用管道需要注意以下4种特殊情况(假设都是阻塞I/0操作,没有设置O_NOBLOCK标志) :

  1. 如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭,而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
  4. 如果有指向管道读端的文件描述符没关闭,而持有管道读端的进程也没从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

2.管道(popen和pclose)

这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭管道的不使用端,exec一个cmd命令,等待命令终止

#include <stdio.h>

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
  1. popen返回:若成功则为文件指针,若出错则为NULL
  2. pclose返回:command的终止状态,若出错则为-1
  • 函数popen先创建管道、执行fork,然后调用exec以执行command,并且返回一个标准I/0文件指针。
  • 如果type是”r”,则文件指针连接到cmd的标准输出。
  • 如果type是”w” ,则文件指针连接到cmd的标准输入。
  • pclose函数关闭标准I/0流,等待命令执行结束,然后返回cmd的终止状态,如果cmd不能被执行,则pclose返回的终止状态与shell执行exit一样。
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

/**
 * @brief 往管道中写
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const* argv[]) {
    FILE* fp = popen("./upper", "w");  //upper是之前写的小写字母转大写字母的程序
    if (!fp) {
        perror("popen");
        exit(-1);
    }
    fprintf(fp, "hello world\n");

    pclose(fp);
    return 0;
}
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>

/**
 * @brief 从管道中读
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const* argv[]) {
    FILE* fp = popen("cat ./test.txt", "r");
    if (!fp) {
        perror("popen");
        exit(-1);
    }
    int c;
    while (~(c = fgetc(fp))) {
        putchar(toupper(c));
    }

    pclose(fp);
    return 0;
}

3.命名管道FIFO

FIFO IPC机制是利用文件系统中的特殊文件来标识的。可以用mkfifo命令创建一个FIFO文件

mkfifo tube
ls -l tube

也可以使用mkfifo函数创建名称管道:

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

int mkfifo(const char *pathname, mode_t mode);
  1. 返回值:成功返回0,失败返回-1并设置errno
  2. pathname:管道名
  3. mode:管道的权限
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>

/**
 * @brief 创建一名为"tube"的命名管道,权限给定660
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    int rtn = mkfifo("./tube", 0660);
    if (rtn < 0) {
        perror("mkfifo");
        exit(-1);
    }

    return 0;
}

FIFO文件在磁盘上没有数据块(有inode但是没有data blocks),仅用来标识内核中的一条通道,各进程可以打开这个文件进行read/write,实际上是在读写内核通道(根本原因在于这个file结构体所指向的read、write函数和常规文件不一样),这样就实现了进程间通信。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * @brief 往FIFO中写数据
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    int fd = open("./tube", O_WRONLY);
    if (fd < 0) {
        perror("open");
        exit(-1);
    }

    write(fd, "hello world\n", 12);

    close(fd);
    return 0;
}
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * @brief 从FIFO中读数据
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    int fd = open("./tube", O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(-1);
    }

    char buff[20];
    ssize_t n = read(fd, buff, 20);
    write(1, buff, n);

    close(fd);
    return 0;
}

注:如果往管道中write设置O_NONBLOCK,那么必须先从管道中read必须是有阻塞读取,然后write会报错:open: No such device or address

4.共享内存

共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是比较快的一种IPC。

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

key_t ftok(const char *pathname, int proj_id);
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
Linux系统编程笔记(7)——进程体系与进程管理(2)-萤火
(1)ftok
key_t ftok(const char *pathname, int proj_id);

ftokpathnameproj_id转换为IPC一个32位的key

  1. pathname:必须为调用进程可以访问的。
  2. proj_id:低八位有效,且为非零值。
  3. 返回值:成功则返回创建的key,失败返回-1并设置errno
(2)shmget
int shmget(key_t key, size_t size, int shmflg);

shmget用于分配共享内存字段。

  1. key:用来标识共享内存,可由ftok()生成。
  2. size:该共享存储段的最小值,如果正在创建一个新段(一般在服务器中),则必须指定其size(实际总会分配PAGE_SIZE,也就是4k的整数倍)。如果正在存访一个现存的段(一个客户机),则将size指定为0。
  3. shmflg:参数有IPC_CREATIPC_EXCL,最为重要的是在shmflg中指明访问权限,跟openmode参数一样。否则会出现permission denied等错误。
  4. 返回值:若成功则为共享内存ID,可以类比做内核中的文件描述符fd;若出错则为-1并设置errno

创建成功后可以使用ipcs命令查看。还可以使用ipcrm -m shmid释放该共享内存。

(3)shmat
void *shmat(int shmid, const void *shmaddr, int shmflg);

一旦创建了一个共享存储段,进程就可调用shmat将其连接到它的地址空间中。

  1. 返回值:若成功则为指向共享存储段的指针,若出错则为-1并设置errno

共享存储段连接到调用进程的哪个地址上与addr参数以及在flag中是否指定SHM_RND位有关

  • 如果addrNULL,则此段连接到由内核选择的第一个可用地址上;
  • 如果addr位非NULL,并且没有制定SHM_RND,则此段连接到addr所指定的地址上;
  • 如果addr非0,并且指定了SHM_RND,则此段连接到(addr-(addr mod SHM_RND)所表示的地址上。SHM_RND命令的意思是:取整。SHM_LBA的意思是:低边界地址倍数,它总是2的乘方。该算式是将地址向下取最近的1个SHM_LBA的倍数。

一般应制定addrNULL,一边由内核选择地址。

(4)shmdt
int shmdt(const void *shmaddr);

调用当对共享存储段的操作已经结束时,则调用shmdt脱接该段。
注意:这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用shmctl(带命令IPC_RMID)去删除它。

  1. shmaddr:之前调用shmat时的返回值
  2. 返回值:若成功则返回0,若出错则返回-1并设置errno
(5)shmctl
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

调用shmctl函数对共享存储段执行多种操作。

  1. cmd:指定下列5种命令中一种,使其在shmid指定的段上执行:
    • IPC_STAT:对此段取shmid_ds结构,并存放在由buf指向的结构中。
    • IPC_SET:按buf指向的结构中的值设置与此段相关结构中的三个字段:shm_perm.uidshm_perm.gid以及shm_perm.mode。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuidshm_perm.uid的进程;另一种是具有超级用户特权的进程。
    • IPC_RMID:从系统中删除该共享存储段。因为每个共享存储段有一个连接计数(shm_nattchshmid_ds结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用shmat与该段连接。此命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuidshm_perm.uid的进程;另一种是具有超级用户特权的进程。
    • SHM_LOCK:锁住共享存储段。此命令只能由超级用户执行。(禁止换出)
    • SHM_UNLOCK解锁共享存储段。此命令只能由超级用户执行。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
    key_t key = ftok("./upper", 9);  //ftok需要绑定到一个已存在的文件上
    if (key < 0) {
        perror("ftok");
        exit(-1);
    }

    printf("key=0x%x\n", key);
    //分配20个字节,权限666
    int shmid = shmget(key, 20, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget\n");
        exit(-2);
    }
    printf("shmid=%d\n", shmid);
    //在用户空间中开辟一段内存然后映射到共享内存
    void *shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr < 0) {
        perror("shmat\n");
        exit(-3);
    }
    printf("shmp=%p\n", shmaddr);
    //将shmp指向的内存区的前20个字节设为'\0'
    bzero(shmaddr, 20);

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-4);
    }
    if (pid) {
        //父进程
        while (1) {
            //往共享内存中写数据
            scanf("%s", (char *)shmaddr);
            if (!strcmp(shmaddr, "quit")) {
                break;
            }
        }
        wait(NULL);
    } else {
        //子进程
        while (1) {
            if (!strcmp(shmaddr, "quit")) {
                break;
            }
            //从共享内存中读数据
            if (*(char *)shmaddr) {
                printf("child process read: %s\n", (char *)shmaddr);
                bzero(shmaddr, 20);
            }
            sleep(1);
        }
    }

    //取消映射关系
    shmdt(shmaddr);

    return 0;
}

5.消息队列

系统内核维护了一个存放消息的队列,不同用户可以向队列中发送消息,或者从队列中接收消息

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

key_t ftok(const char *pathname, int proj_id);
int msgget(key_t key, int msgflg)
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
Linux系统编程笔记(7)——进程体系与进程管理(2)-萤火
(1)msgget
int msgget(key_t key, int msgflg)

系统创建或获取消息队列

  1. key:使用ftok()获取到的key
  2. msgflgIPC_CREAT | 0666,见shmget()
  3. 返回值:成功返回消息队列的ID,失败返回-1并设置errmo

创建成功后可以使用ipcs命令查看。还可以使用ipcrm -q msqid释放该消息队列。

(2)msgsnd

往队列里发送一条消息。此操作被中断后不会被重启(信号处理中SA_RESTART)。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  1. msqid:消息队列ID
  2. msgp:消息,通常为下面的结构体
    struct msgbuf {
    long mtype; /* 消息类型,必须>0 */
    char mtext[1]; /* 消息数据,可自定义类型与大小 */
    };
  3. msgsz:消息的长度,指的是消息数据的长度
  4. msgflg
    • IPC_NOWAIT:不阻塞
    • MSG_EXCEPT:接收不检测mtype
    • MSG_NOERROR:消息数据过长时会截断数据
  5. 返回值:成功则返回0,失败则返回-1并设置errno

(3)msgrcv

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  1. 返回值:成功返回msgp.mtext的长度,出错返回-1并设置errno
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define MSG_LEN 20

typedef struct msgbuf {
    long mtype;
    char mtext[MSG_LEN];
} msgbuf;

int main(int argc, char const* argv[]) {
    key_t key = ftok("./upper", 9);  //ftok需要绑定到一个已存在的文件上
    if (key < 0) {
        perror("ftok\n");
        exit(-1);
    }
    printf("key=%#x\n", key);
    int msqid = msgget(key, IPC_CREAT | 0666);
    if (msqid < 0) {
        perror("msgget\n");
        exit(-2);
    }
    printf("msqid=%d\n", msqid);

    //发消息
    msgbuf msgbuf;
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-3);
    }
    if (pid) {
        //parent process
        //send message
        msgbuf.mtype = 1;
        strncpy(msgbuf.mtext, "hello\n", MSG_LEN);
        msgsnd(msqid, &msgbuf, MSG_LEN, 0);
        msgbuf.mtype = 2;
        strncpy(msgbuf.mtext, "world\n", MSG_LEN);
        msgsnd(msqid, &msgbuf, MSG_LEN, 0);
        wait(NULL);
    } else {
        //child process
        //recv message
        ssize_t n = msgrcv(msqid, &msgbuf, MSG_LEN, 2, 0);
        printf("msg.type = %ld\nmsg.text: %s\nmsg length=%ld\n", msgbuf.mtype, msgbuf.mtext, n);
        n = msgrcv(msqid, &msgbuf, MSG_LEN, 1, 0);
        printf("msg.type = %ld\nmsg.text: %s\nmsg length=%ld\n", msgbuf.mtype, msgbuf.mtext, n);
    }

    return 0;
}