08-进程与IPC(进程控制、信号、管道、命名管道、信号量、共享内存、消息队列)

进程及通信

STAT代码 说明
S 睡眠状态,等待某个事件发生,如信号
R 运行。严格说“可运行”
D 不可中断的睡眠(等待)。通常是等待输入输出
T 停止
Z 死进程 僵尸进程
N 低优先级任务,nice
W 分页,不适用于2.6版本起的内核
s 进程是会话期首进程
+ 进程属于前台进程组
l 进程是多线程的
< 高优先级任务

init进程是系统运行启动的第一个进程,进程号为1,用于启动管理其他进程。Linux中进程都是由父进程启动的,呈现一个树的结构,init进程就是root。

PID    TTY    STAT    TIME    COMMAND
 1      ?      Ss      0:03     init [5]

用户登录过程:

init 进程为每个用户用于登录的串行终端启动一次getty程序。对应ps命令输出如下:

9619    tty2    Ss+    0:00     /sbin/mingetty    tty2

getty 进程等待来自终端的操作,向用户显示登陆提示符,然后将控制移交给登录程序,登录程序设置用户环境,最后启动一个shell。用户退出系统时,init 进程再次启动另一个getty进程。

一个 C 程序是如何启动和终止的

内核使程序执行的唯一方法是调用一个exec函数。
进程自愿终止的唯一方式是显示或隐式(调用exit函数)的调用 _exit _Exit。
进程也可以非自愿的由一个信号终止。

环境表

每个程序都接收一张环境表,与参数表一样环境表也是一个字符指针数组。全局变量environ包含了该数组指针地址。

#include <unistd.h>
extern char **environ;

进程终止

exit和_Exit是由ISO C说明的,_exit是由 POXI.1 说明的。
exit总是执行一个IO标准库的清理关闭操作,对于所有打开流调用fclose。

return 是结束当前函数,exit 结束当前线程。

终止处理函数

ISO C 规定一个进程最多可以登记32个终止处理函数,exit函数按照与注册顺序相反的顺序调用这些函数,如果一个函数登记多次则也会调用多次。

启动新进程

#include <stdlib.h>

int system(const char *string); // 启动一个shell并执行命令,shell与原来程序同时运行。不建议使用

替换进程映像

exec 系列函数由一组相关函数组成,它们在启动方式与程序参数的表达方式上各有不同。exec可将当前进程替换为新进程。启动新程序后原有程序就不再运行,比system更有效。例如:启动一个有受限使用策略的程序前检查用户凭证。

#include <unistd.h>

// 此全局变量用于将一个值传递到新的程序环境中
char **environ;

// 程序启动时将参数传给main函数
// 这些函数通常使用 execve 实现
// 以p结尾的函数搜索PATH环境变量来查找新程序的可执行文件路径

// 这三者参数可变,参数以空指针结束 (char *)0
int execl (const char *__path, const char *__arg, ...);
int execlp (const char *__file, const char *__arg, ...);
int execle (const char *__path, const char *__arg, ...);

int execv (const char *__path, char *const __argv[]);
int execvp (const char *__file, char *const __argv[]);
int execvpe (const char *__file, char *const __argv[], 
             char *const __envp[]); // envp 用于传递字符串数组作为新程序的环境变量
#include <unistd.h>

char *const ps_argv[] = {"ps", "ax", 0};
char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", 0};

execl("/bin/ps", "ps", "ax", 0);
execlp("ps", "ps", "ax", 0);
execle("/bin/ps", "ps", "ax", 0, ps_envp);

execv("/bin/ps", pa_argv);
execvp("ps", ps_argv);
execve("/bin/ps", ps_argv, ps_envp);

执行后的进程表中有ps ax项却没有此程序本身,进程表中ps命令的PID与PPID与原进程的一样。事实是,运行中的程序开始执行exec调用中指定的新的可执行文件中的代码。出现错误时exec函数返回-1,并设置错误变量errno。exec启动新进程继承许多原进程特性,如文件描述符,除非他们“执行时关闭标志 close on exec flag”被置位;原进程中目录流被关闭。

复制进程印象

使用fork创建一个新进程,这个系统调用完全复制当前进程,但新进程有自己的数据空间、环境、文件描述符。forkexec结合就是创建新进程所需的一切。

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

pid_t fork(void);

父进程中fork调用返回的是新的子进程PID,子进程中返回的是0。如果失败返回-1,子进程数目超出限制时errno将被设置为EAGAIN,没有足够空间时设置为ENOMEM。

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

int main() {
    pid_t pid;
    char *message;
    int n;
    
    pid = fork();
    switch(pid){
    	case -1:
        	perror("fork failed");
        	exit(1);
        case 0:
            puts("This is the child");
            break;
        default:
            puts("This is the parent");
            break;
    }
    exit(0);
}

等待进程

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

pid_t wait(int *stat_loc);

wait 系统调用将暂停父进程直到它的子进程结束为止。这个调用返回子进程的PID,通常是已结束运行的子进程的PID。状态信息用于了解子进程的退出状态,即main的返回值或子进程中的exit函数退出码,写入stat_loc指针。

WEXITSTATUS(status)   // 若WIFEXITED非零,它返回子进程退出码
WTERMSIG(status)      // 若WIFSIGNALED非零,返回一个信号代码
WSTOPSIG(status)      // 若WIFSTOPPED非零,返回一个信号代码
WIFEXITED(status)     // 若子进程正常结束返回一个非零值
WIFSIGNALED(status)   // 若子进程因一个非捕获信号而终止,取一个零值
WIFSTOPPED(status)    // 若子进程意外终止,取一个非零值
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    char *message;
    int n;
    int exit_code;

    pid = fork();
    switch (pid) {
    case -1:
        printf("fork failed\n");
        break;
    case 0:
        printf("This is child\n");
        break;
    default:
        printf("This is parent\n");
        break;
    }

    if (pid != 0) {
        int stat_val;
        pid_t child_pid;

        child_pid = wait(&stat_val);

        printf("Child has finished: PID = %d\n", child_pid);
        if (WIFEXITED(stat_val)) {
            printf("Child exited with code %d\n", WEXITSTATUS(stat_val));
        } else {
            printf("Child terminated abnormally\n");
        }
    }

    return 0;
}

僵尸进程

子进程终止时其与父进程的关联任然保留,直到父进程也正常终止或父进程调用wait才结束。因此子进程即使不运行,它依然存在于系统中,因为它的退出码还要保存起来以备父进程wait调用,在进程表中代表子进程的表项不会立即释放。这时它成为一个死进程或僵尸进程。

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

int main() {
    pid_t pid;
    char *message;
    int n;
    int exit_code;

    pid = fork();
    switch (pid) {
    case -1:
        printf("fork failed\n");
        break;
    case 0:
        printf("This is child\n");
        break;
    default:
        printf("This is parent\n");
        sleep(2);
        execlp("ps", "ps", "aux", (char *)0);
        break;
    }
    return 0;
}
/*
This is parent
This is child
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
dev       1467  0.0  0.0  10616  3288 pts/5    R+   22:26   0:00 ps aux
dev       1468  0.0  0.0      0     0 pts/5    Z+   22:26   0:00 [main.exe] <defunct>  //僵尸进程
*/

如果此时父进程异常终止,子进程将把PID为1的进程(即init)作为自己的父进程。僵尸进程将一直留在进程表中直到被init进程发现并释放。进程表越大这一过程就越慢。尽量避免其消耗资源。

还有令一个系统调用可用于等待子进程的结束 waitpid

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

// pid 指定要等待的子进程PID,若为-1,则返回任一子进程的信息
// stat_loc 不为空时写入状态信息
// options 用于改变函数行为,最有用的为WNOHANG,防止waitpid调用将程序挂起
__pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options);

// 定期检查某个特定子进程是否终止
// 没有结束或意外终止返回 0
// 失败返回 -1 并设置 errno
waitpid(child_pid, (int *)0, WNOHANG);

输入输出重定向

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

int main()
{
    char *filename = "xxx";

    // 将标准输入重定向到文件
    if (!freopen(filename, "r", stdin)) {
        fprintf(stderr, "could not redirect\n");
        exit(2);
    }

    // 原程序从stdin中读取数据,现在从filename中读取
    execl("./program", "program", 0);
    
    // execl 执行成功后不会执行以下程序
    perror("execl failed\n");
    return 0;
}

进程间通信

信号

信号是由于某些错误条件而生成的,如内存段冲突、浮点处理器错误或非法指令等。他们由shell和终端处理器生成来引起中断。还可以作为进程间传递消息或修改行为的一种方式,明确的由一个进程发给一个进程。

信 号 默认行为 描 述 信号值
SIGABRT 生成 core 文件然后终止进程 这个信号告诉进程终止操作。ABRT 通常由进程本身发送,即当进程调用 abort() 函数发出一个非正常终止信号时 6
SIGALRM 终止 警告时钟 14
SIGBUS 生成 core 文件然后终止进程 当进程引起一个总线错误时,BUS 信号将被发送到进程。例如,访问了一部分未定义的内存对象 10
SIGCHLD 忽略 当了进程结束、被中断或是在被中断之后重新恢复时,CHLD 信号会被发送到进程 20
SIGCONT 继续进程 CONT 信号指不操作系统重新开始先前被 STOP 或 TSTP 暂停的进程 19
SIGFPE 生成 core 文件然后终止进程 当一个进程执行一个错误的算术运算时,FPE 信号会被发送到进程 8
SIGHUP 终止 当进程的控制终端关闭时,HUP 信号会被发送到进程 1
SIGILL 生成 core 文件然后终止进程 当一个进程尝试执行一个非法指令时,ILL 信号会被发送到进程 4
SIGINT 终止 当用户想要中断进程时,INT 信号被进程的控制终端发送到进程 2
SIGKILL 终止 发送到进程的 KILL 信号会使进程立即终止。KILL 信号不能被捕获或忽略 9
SIGPIPE 终止 当一个进程尝试向一个没有连接到其他目标的管道写入时,PIPE 信号会被发送到进程 13
SIGQUIT 终止 当用户要求进程执行 core dump 时,QUIT 信号由进程的控制终端发送到进程 3
SIGSEGV 生成 core 文件然后终止进程 当进程生成了一个无效的内存引用时,SEGV 信号会被发送到进程 11
SIGSTOP 停止进程 STOP 信号指示操作系统停止进程的执行 17
SIGTERM 终止 发送到进程的 TERM 信号用于要求进程终止 15
SIGTSTP 停止进程 TSTP 信号由进程的控制终端发送到进程来要求它立即终止 18
SIGTTIN 停止进程 后台进程尝试读取时,TTIN 信号会被发送到进程 21
SIGTTOU 停止进程 后台进程尝试输出时,TTOU 信号会被发送到进程 22
SIGUSR1 终止 发送到进程的 USR1 信号用于指示用户定义的条件 30
SIGUSR2 终止 同上 31
SIGPOLL 终止 当一个异步输入/输出时间事件发生时,POLL 信号会被发送到进程 23
SIGPROF 终止 当仿形计时器过期时,PROF 信号会被发送到进程 27
SIGSYS 生成 core 文件然后终止进程 发生有错的系统调用时,SYS 信号会被发送到进程 12
SIGTRAP 生成 core 文件然后终止进程 追踪捕获/断点捕获时,会产生 TRAP 信号。 5
SIGURG 忽略 当一个 socket 有紧急的或是带外数据可被读取时,URG 信号会被发送到进程 16
SIGVTALRM 终止 当进程使用的虚拟计时器过期时,VTALRM 信号会被发送到进程 26
SIGXCPU 终止 当进程使用的 CPU 时间超出限制时,XCPU 信号会被发送到进程 24
SIGXFSZ 生成 core 文件然后终止进程 当文件大小超过限制时,会产生 XFSZ 信号 25

SIGCHLD对管理子进程很有用。SIGCONT用于让进程恢复并继续执行,shell脚本通过它来控制作业。

使用kill命令发送信号,程序使用signal库函数处理。

#include <signal.h>

typedef void (*__sighandler_t) (int);

// sig 参数指定要捕获或忽略的信号
// __handler 是一个信号处理函数
// 可以使用SIG_IGN表忽略信号,SIG_DFL表示恢复默认行为
__sighandler_t signal (int __sig, __sighandler_t __handler);  // 旧接口 不推荐使用

发送信号

与kill命令相同,没有权限时调用失败,失败的原因常为目标进程为其他用户所有。

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

int kill (__pid_t __pid, int __sig);

成功返回0,普通用户只能发给自己的进程,超级用户可发给所有进程。失败时返回-1并设置errno。

#include <unistd.h>

// 在一定秒数后发送一个SIGALRM信号
unsigned int alarm(unsigned int seconds);

// 将程序挂起,直到接收到一个信号
int pause();

下面程序fork一个子进程,子进程休眠5秒后向父进程发一个信号。

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

static int alarm_fired = 0;

// 模拟一个闹钟
void ding(int sig) { alarm_fired = 1; }

int main(int argc, char const *argv[])
{
    pid_t pid;

    pid = fork();
    switch (pid) {
    case -1:
        perror("fork failed\n");
        exit(1);
        break;
    case 0:
        // 子进程向父进程发送一个信号
        sleep(5);
        kill(getppid(), SIGALRM);
        exit(0);
        break;
    default:
        break;
    }

    printf("waiting for alarm to go off\n");
    
    // 设置信号处理函数
    signal(SIGALRM, ding);

    // pause函数,将程序挂起,直到由一个信号出现。
    // 信号出现后预设的信号处理函数开始运行,程序也恢复正常执行
    pause();

    if (alarm_fired) {
        printf("ding\n");
    }

    printf("done\n");
    return 0;
}

处理信号时要小心,因为信号可能发生在pause调用之前,这回使程序无限期等待。

sigaction

更健壮的信号接口

#include <signal.h>

// 设置于信号关联的动作,成功返回0,失败返回-1
int sigaction (int __sig, 
               const struct sigaction *__restrict __act, // 设置新动作,为空时无操作
               struct sigaction *__restrict __oact); // 不为空则写入旧动作

struct sigaction  // 最少包含以下成员
{
	// 指向收到信号时的处理函数,可设为特殊的 SIG_IGN SIG_DFL
    void (*) (int) sa_handler;
    
    // 指定一个信号集,调用sa_handler前该信号集被加入到进程信号屏蔽字
    // 这是一组将被阻塞且不会被传递的信号
    // 设置信号屏蔽字可防止前面看到的信号在它的处理函数还未结束时就被接收到的情况,使用这一字段可以消除竞态条件
    sigset_t sa_mask;
    
    // sigaction设置的信号处理函数在默认情况下不被重置
    // 包含SA_RESTHAND 实现对信号处理重置的效果
    int sa_flags;
    /*
    可取值
    SA_NOCLDSTOP    子进程暂停时不产生SIGCHLD信号,终止时仍会产生
    SA_RESETHAND    将对此信号的处理方式在信号处理函数的入口处重置为SIG_DFL
    SA_RESTART      重启可中断函数而不是被信号中断给出EINTR错误。程序中许多系统调用都是可中断的。
                    就是说,接收到一个信号时,他们将返回一个错误并将errno设置为EINTR,表明函数是因为一个信号而返回的。
    SA_NODEFER      捕捉到信号时不将其添加到信号屏蔽字
    */
};

void func(int sig) { printf("I got signal %d\n", sig); }

int main(int argc, char const *argv[])
{
    struct sigaction act;

    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, 0);

    pause();
    return 0;
}
// 按下 ctrl+c (SIGINT) 可以得到输出

信号集

#include <signal.h>

// 成功返回0,失败返回-1并设置errno

/* Clear all signals from SET.  */
extern int sigemptyset (sigset_t *__set);

/* Set all signals in SET.  */
extern int sigfillset (sigset_t *__set);

/* Add SIGNO to SET.  */
extern int sigaddset (sigset_t *__set, int __signo);

/* Remove SIGNO from SET.  */
extern int sigdelset (sigset_t *__set, int __signo);

/* Return 1 if SIGNO is in SET, 0 if not.  */  // 信号无效时返回-1
extern int sigismember (const sigset_t *__set, int __signo);

操作屏蔽字

/* Get and/or change the set of blocked signals.  */
extern int sigprocmask (int __how,   // 指定修改进程屏蔽字方法
                        const sigset_t *__restrict __set,   // 设置新屏蔽字
                        sigset_t *__restrict __oset);       // 获得旧屏蔽字
/*
how的取值
    1. SIG_BLOCK    将参数添加到屏蔽字中
    2. SIG_SETMASK  将屏蔽字设置为参数
    3. SIG_UNBLOCK  从屏蔽字中删除set中信号 
*/

如果一个信号被进程阻塞,他就不会传递给进程,但会停留在待处理状态。程序可以通过调用sigpending来查看它阻塞的信号中有哪些正停留在待处理状态。此函数将被阻塞的信号中待处理状态的的一组信号写到参数set中。如果程序需要处理信号,同时又要控制信号处理函数的调用时间,这个函数很有用。

#include <signal.h>

int sigpending(sigset_t *set);

进程可以通过sigSuspend挂起自己的执行,直到信号集中的一个信号到达为止。是pause的更通用形式。其更新进程的屏蔽字然后挂起程序的执行。在信号处理函数结束后继续执行。若接收到的信号终止了程序,sigsuspend就不会返回,若没有终止则返回-1将errno设置为EINTR。

#include <signal.h>

int sigsuspend(const sigset_t *sigmask);

管道

将一个程序的输出作为另一个程序的输入,在shell中管道是通过管道字符完成的 ps aux|grep ls

进程管道,实现单向通信,两个管道即可实现双向通信。popen调用的过程中先启动一个shell即系统中sh命令,再将command作为一个参数传递给他,会有两个效果:

  1. 类UNIX系统中,所有参数拓展由shell完成。所以在程序启动前各种shell拓展全部完成,如通配符 *.c 指代哪些文件。它允许我们通过popen启动非常复杂的shell命令。其他的创建进程函数(如 execl)则要自己取完成shell拓展
  2. 每个调用要启动目标程序和shell两个进程,成本略高,速度要慢。
  3. 管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,在进程fork之后,这个管道就能在父子进程间使用了。FIFO没有这种局限。UNIX域套接字没有这种局限。
#include <stdio.h>

/*
允许一个进程将一个程序作为新进程启动,并可以传递给它或通过它接收数据
popen 先启动一个fork,然后调用exec执行命令,并返回一个标准文件io
__command为运行程序名称与参数,__modes为 r 或 w
r 模式,被调用程序的stdout可被调用程序读取
w 模式,调用程序可以用fwrite向被调用程序发送数据,被调用程序在stdin上读取
*/
FILE *popen (const char *__command, const char *__modes);

/*
启动进程结束时使用其关闭关联的文件流,pclose调用等待到进程结束后才返回。
返回值通常是关闭文件流所在进程的退出码,若在调用pclose前调用了wait语句,调用进程的退出状态丢失,此时返回-1并设置errno为ECHILD
*/
int pclose (FILE *__stream);

输入送往popen

#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE *read_fp;
    char buffer[BUFSIZ + 1];
    int chars_read;
    memset(buffer, '\0', sizeof(buffer));
    
    read_fp = popen("uname -a", "r");
    
    if (read_fp != NULL) {
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        if (chars_read >= 0) {
            printf("Output was:-\n%s\n", buffer);
        }
        pclose(read_fp);
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}

输出送往popen

#include <stdio.h>

int main(int argc, char const *argv[])
{
    FILE *write_fp;
    char buffer[BUFSIZ + 1];

    sprintf(buffer, "This is a string.\n");

    write_fp = popen("od -c", "w");
    if (write_fp != NULL) {
        fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
        pclose(write_fp);
        exit(EXIT_SUCCESS);
    }

    return 0;
}

有时不知道输出数据的长度,可以分多次读取

int main(int argc, char const *argv[])
{
    FILE *read_fp;
    char buffer[BUFSIZ + 1];
    int chars_read;

    memset(buffer, '\0', sizeof(buffer));

    read_fp = popen("ps ax", "r");
    if (read_fp != NULL) {
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        while (chars_read > 0) {
            buffer[chars_read - 1] = '\0';
            printf("Reading %d:-\n %s\n", BUFSIZ, buffer);
            chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        }
        pclose(read_fp);
        exit(EXIT_SUCCESS);
    }

    exit(EXIT_FAILURE);
}

更底层的 pipe 调用

通过这个函数在两个程序间传数据无需启动shell解释请求的命令,同时还提供了对读写的更多控制。

#include <unistd.h>

int pipe (int __pipedes[2]);

在数组中填上两个新的文件描述符后返回0,失败返回-1并设置errno。写到__pipedes[1] 的所有数据都可以从 __pipedes[0] 中读出。管道内有一些内置空间,用于在write和read间保存数据。

int main(int argc, char const *argv[])
{
    int data_processed;
    int file_pipes[2];
    const char some_date[] = "123";
    char buffer[BUFSIZ + 1];

    memset(buffer, '\0', sizeof(buffer));

    if (pipe(file_pipes) == 0) {
        data_processed = write(file_pipes[1], some_date, strlen(some_date));
        printf("Wrote %d bytes.\n", data_processed);

        data_processed = read(file_pipes[0], buffer, BUFSIZ);
        printf("Read %d bytes: %s\n", data_processed, buffer);

        exit(EXIT_SUCCESS);
    }

    exit(EXIT_FAILURE);
}

pipe 与 fork

这个接口可以很好的结合fork创建新进程,fork出的进程拥有与原进程完全相同的资源。

int main(int argc, char const *argv[])
{
    int data_processed;
    int file_pipes[2];
    const char some_date[] = "123";
    char buffer[BUFSIZ + 1];
    pid_t fork_result;

    memset(buffer, '\0', sizeof(buffer));

    if (pipe(file_pipes) == 0) {
        fork_result = fork();
        if (fork_result == -1) {
            fprintf(stderr, "Fork failed.\n");
            exit(EXIT_FAILURE);
        }

        if (fork_result == 0) {
            data_processed = read(file_pipes[0], buffer, BUFSIZ);
            printf("Read %d bytes: %s\n", data_processed, buffer);
            exit(EXIT_SUCCESS);
        } else {
            data_processed = write(file_pipes[1], some_date, strlen(some_date));
            printf("Wrote %d bytes\n", data_processed);
            exit(EXIT_SUCCESS);
        }
    }

    exit(EXIT_FAILURE);
}

pipe fork exec

当在子进程中通过exec启动程序时因原进程被新进程替换,原有资源丢失。为解决问题将文件描述符(其实就是一个数字)为参数传递给exec启动的程序。

int main(int argc, char const *argv[])
{
    int data_processed;
    int file_pipes[2];
    const char some_date[] = "123456789";
    char buffer[BUFSIZ + 1];
    pid_t fork_result;

    memset(buffer, '\0', sizeof(buffer));

    if (pipe(file_pipes) == 0) {
        fork_result = fork();
        if (fork_result == -1) {
            fprintf(stderr, "Fork failed.\n");
            exit(EXIT_FAILURE);
        }

        if (fork_result == 0) {
            sprintf(buffer, "%d", file_pipes[0]);
            execl("sub_process", "./sub_process", buffer, (char *)0);
            exit(EXIT_FAILURE);
        } else {
            data_processed = write(file_pipes[1], some_date, strlen(some_date));
            printf("%d - wrote %d bytes\n", getpid(), data_processed);
            exit(EXIT_SUCCESS);
        }
    }

    exit(EXIT_FAILURE);
}

子程序

int main(int argc, char const *argv[])
{
    int data_processed;
    char buffer[BUFSIZ + 1];
    int file_descriptor;

    memset(buffer, '\0', sizeof(buffer));
    sscanf(argv[1], "%d", &file_descriptor);
    data_processed = read(file_descriptor, buffer, BUFSIZ);

    printf("%d - read %d bytes: %s\n", getpid(), data_processed, buffer);
    return 0;
}

管道关闭后的操作

没有数据可读时read会阻塞,若另一端被关闭则read调用返回0而不是阻塞。这就使读进程能够像检测文件结束一样对管道进行检测。读无效描述符read返回-1。

跨越fork调用使用管道,会有两个不同的文件描述符用于写数据,分别在父子进程中。只有两个都关闭管道才会被认为是关闭了。

把管道用作标准输入输出

将管道文件描述符设置为0或1可以简化程序。

#include <unistd.h>

// 复制fd,并返回一个全新的fd,总是取最小值
int dup (int __fd);

// 关闭fd2,复制fd,新的fd值由后一个参数指定,或是第一个大于第二个参数的值
int dup2 (int __fd, int __fd2);

在子进程中将标准输入设置为管道输入

int main(int argc, char const *argv[])
{
    int data_processed;
    int file_pipes[2];
    const char some_date[] = "123456789";
    pid_t fork_result;

    if (pipe(file_pipes) == 0) {
        fork_result = fork();
        if (fork_result == (pid_t)-1) {
            printf("Fork failed.\n");
            exit(EXIT_FAILURE);
        }

        if (fork_result == 0) {
            close(0);
            dup(file_pipes[0]);
            close(file_pipes[0]);
            close(file_pipes[1]);

            execlp("od", "od", "-c", (char *)0);
            exit(EXIT_FAILURE);
        } else {
            close(file_pipes[0]);
            data_processed = write(file_pipes[1], some_date, strlen(some_date));
            close(file_pipes[1]);
            printf("%d - wrote %d bytes\n", getpid(), data_processed);
        }
    }

    exit(EXIT_SUCCESS);
}

命名管道 FIFO

用于在不相关程序间传递数据,通过创建一种特殊的管道文件。使用命令mkfifo filename创建,旧版本为mknod filename p。通过state结构的st_mode成员的编码可知文件是否是FIFO类型。

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

// mode_t 为用户权限掩码,可使用 rm 或 unlink 删除
int mkfifo (const char *__path, __mode_t __mode);
int mkfifoat (int __fd, const char *__path, __mode_t __mode);

// mknode与命令行中命令一样,可以创建许多特殊类型的文件。
// 通过它创建一个命名管道需要 dev_t 设为0,mode_t 与 S_IFIFO 按位或
int mknod (const char *__path, __mode_t __mode, __dev_t __dev);

使用 open 打开FIFO文件

不能使用O_RDWR打开文件进行读写操作,FIFO只能进行单向数据传输。open_flagO_NONBLOCK选项不仅改变open调用的处理方式,还改变open调用返回的文件描述符进行的读写请求的处理方式。O_RDONLYO_WRONLYO_NONBLOCK 共有4中组合方式

  1. open(const char *path, O_RDONLY);
    open调用将阻塞,直到一个进程以写的方式打开同一个FIFO
  2. open(const char *path, O_RDONLY | O_NONBLOCK);
    总是成功。即使没有其他进程以写方式打开FIFO,这个open调用也将成功返回
  3. open(const char *path, O_WRONLY);
    open调用将阻塞,直到一个进程以读的方式打开同一个FIFO
  4. open(const char *path, O_WRONLY | O_NONBLOCK);
    总是会立刻返回,如果有一个进程以读方式打开,可以通过文件描述符进行写操作;如果没有进程以读方式打开,将返回一个错误-1,且FIFO也不会被打开,errno设为 ENXIO。

close的调用不受 O_NONBLOCK 的影响

// 生产者
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)

int main(int argc, char const *argv[])
{
    int pipe_fd;
    int res;
    int open_mode = O_WRONLY;
    int bytes_sent = 0;
    char buffer[BUFFER_SIZE + 1];

    if (access(FIFO_NAME, F_OK) == -1) {
        res = mkfifo(FIFO_NAME, 0777);
        if (res != 0) {
            fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);
            exit(EXIT_FAILURE);
        }
    }

    printf("Process %d opening FIFO O_WRONLY\n", getpid());
    pipe_fd = open(FIFO_NAME, open_mode);
    printf("Process %d result %d\n", getpid(), pipe_fd);

    if (pipe_fd != -1) {
        while (bytes_sent < TEN_MEG) {
            res = write(pipe_fd, buffer, BUFFER_SIZE);
            if (res == -1) {
                fprintf(stderr, "Write error on pipe\n");
                exit(EXIT_FAILURE);
            }
            bytes_sent += res;
        }
        close(pipe_fd);
    } else {
        exit(EXIT_FAILURE);
    }

    printf("Process %d finished\n", getpid());

    exit(EXIT_SUCCESS);
}
// 消费者
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF

int main(int argc, char const *argv[])
{
    int pipe_fd;
    int res;
    int open_mode = O_RDONLY;
    char buffer[BUFFER_SIZE + 1];
    int bytes_read = 0;

    memset(buffer, '\0', sizeof(buffer));

    printf("Process %d opening FIFO O_RDONLY\n", getpid());
    pipe_fd = open(FIFO_NAME, open_mode);
    printf("Process %d result %d\n", getpid(), pipe_fd);

    if (pipe_fd != -1) {
        do {
            res = read(pipe_fd, buffer, BUFFER_SIZE);
            bytes_read += res;
        } while (res > 0);
    } else {
        exit(EXIT_FAILURE);
    }

    printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
    return 0;
}

案例:
目标

使用命令

得到

XSI IPC

基于 System V 的IPC函数。
使用 ipcsipcrm 命令查看和删除 IPC 对象。


/* 使用已存在的文件名和项目id产生一个键。
 * ftok通常采用文件的state中某些字段,可能丢失信息。
 * 这意味着对于两个不同的文件路径名,如果使用同一项目id,可能产生相同的键。
 */
key_t ftok (const char *__pathname, int __proj_id);

信号量

管理对资源的访问(这里比线程间的信号量通信更加通用)。它是一个特殊变量,只允许它进行等待和发送信号这两种操作。

所有的Linux信号量函数都是针对成组的通用信号量进行操作,而不是一个二进制信号量。

缺点:

#include <sys/sem.h>

/*
参数key的作用很像一个文件名,代表程序可能使用的某个资源,若多个程序使用相同的key值,它将负责协调工作。
由semget返回的并用在其他共享内存函数中的标识符也与fopen返回的FILE*文件流相似,进程需要通过它来访问共享文件。
类似文件的使用,不同进程可以用不同的信号量标识符指向同一个信号量。
semget 创建一个新信号量或取得一个已有的信号量,只有semget才直接使用 *信号量键*,其他函数都是使用其返回的 *信号量标识符*
*/
// 返回值就是其他信号量函数用的信号量标识符
int semget (key_t __key, // 信号量键,不同相关的进程通过它访问同一个信号量
            int __nsems, // 需要的信号量数目,一般取1
            int __semflg); // 与open函数的标志位相似,低端的9个比特是该信号量的权限,类似于文件的访问权限


/*
用于改变信号量的值。为了避免出现因使用多个信号量而可能发生的竞争现象,调用的一切动作都是一次性完成的
*/
int semop (int __semid,            // semget返回的信号标识符
           struct sembuf *__sops, 
           size_t __nsops);

struct sembuf
{
  unsigned short int sem_num;	// 信号量编号,除非使用一组编号,否则为 0
  short int sem_op;		// 一次操作中需要改变的数值,一般为 -1 即P,或 +1 即v
  short int sem_flg;		// 一般为SEM_UNDO,使os跟踪当前进程对这个信号量的修改情况。
                                // 若进程没有释放该信号量就终止,os会自动释放该进程持有的信号量。
                                // 如果非SEM_UNDO,则一定要注意保持设置的一致性。
};


/*
直接设置信号量信息,成功返0,失败返-1。在第一次使用前设置
*/
int semctl (int __semid,      // semget 返回的信号量标志符
            int __semnum,     // 信号量编号
            int __cmd,        // 将要采取的动作,有许多,这里最常用的两个:
                              //   1. SETVAL 把信号量设置为 union semun 中的val,用于第一次使用前设置
                              //   2. IPC_RMID 删除一个已无需继续使用的信号量标识符
            ...);             // 第四个参数 union semun 结构

union semun // 最少有以下三个成员,这个结构经常需要自己定义,ubuntu的代码注释中有提示,位于sem.h头文件
{
  int val;                     // <= value for SETVAL
  struct semid_ds *buf;        // <= buffer for IPC_STAT & IPC_SET
  unsigned short int *array;   // <= array for GETALL & SETALL
};

semctl 的 cmd 参数

一个例子,在进入退出同步区时会输出字符 O 或 X

#include <fcntl.h>
#include <limits.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#include "semun.h"  // 某些Linux版本下需要自定义

static int set_semvalue();
static void del_semvalue();
static int semaphore_p();
static int semaphore_v();

static int sem_id;

int main(int argc, char const *argv[])
{
    int i;
    int pause_time;
    char op_char = 'O';

    srand((unsigned int)getpid());

    sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT);

    if (argc > 1) {
        if (!set_semvalue()) {
            fprintf(stderr, "Failed to initialize semaphore\n");
            exit(EXIT_FAILURE);
        }
        op_char = 'X';
        sleep(2);
    }

    for (i = 0; i < 10; i++) {
        if (!semaphore_p()) {
            exit(EXIT_FAILURE);
        }
        printf("%c", op_char);
        fflush(stdout);

        pause_time = rand() % 3;
        sleep(pause_time);

        printf("%c", op_char);
        fflush(stdout);

        if (!semaphore_v()) {
            exit(EXIT_FAILURE);
        }
        pause_time = rand() % 2;
        sleep(pause_time);
    }

    printf("\n%d - finished\n", getpid());

    if (argc > 1) {
        sleep(10);
        del_semvalue();
    }

    exit(EXIT_SUCCESS);
}

static int set_semvalue()
{
    union semun sem_union;

    sem_union.val = 1;
    if (semctl(sem_id, 0, SETVAL, sem_union) == -1) {
        return 0;
    }
    return 1;
}

static void del_semvalue()
{
    union semun sem_union;

    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1) {
        printf("Failed to delete semaphore\n");
    }
}

static int semaphore_p()
{
    struct sembuf sem_b;

    sem_b.sem_num = 0;
    sem_b.sem_op = -1;
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) {
        printf("semaphore_p failed\n");
        return 0;
    }
    return 1;
}

static int semaphore_v()
{
    struct sembuf sem_b;

    sem_b.sem_num = 0;
    sem_b.sem_op = 1;
    sem_b.sem_flg = SEM_UNDO;
    if (semop(sem_id, &sem_b, 1) == -1) {
        printf("semaphore_p failed\n");
        return 0;
    }
    return 1;
}

共享内存

用于在程序间高效共享数据,通过将不同进程的逻辑地址映射到同一段物理地址,以实现大量数据共享。
可使用信号量同步共享存储访问。

#include <sys/shm.h>

/*
创建共享内存,通过key获得共享内存标识符,用于后续的共享内存函数。特殊的key IPC_PRIVATE 用于创建一个只属于创建进程的共享内存。
key 与 共享内存标识符 类似文件名与文件标识符
成功返回共享内存标识符,失败返回-1
*/
int shmget (key_t __key, 
            size_t __size,   // 以字节为单位的共享内存容量
            int __shmflg);   // 9个比特的权限标志,与创建文件时的mode标志一样。IPC_CREAT需要与权限标志位按位与才能创建文件

/*
将共享内存段连接到进程的地址空间中,成功返回一个指向内存的第一个字节的指针,失败返回-1
成功时内核将关联的 shmid_ds 结构中的 shm_nattch 计数器加1
*/
void *shmat (int __shmid,            // 共享内存标识符
             const void *__shmaddr,  // 共享内存连接到当前进程中的地址位置,通常是一个空指针
             int __shmflg);          // 位标志,可能取 SHM_RND(与__shmaddr联合控制共享内存地址) 和SHM_RDONLY(内存只读)

/*
将共享内存与当前内存分离,只是分离,没有删除。成功返0,失败返-1
将 shmid_ds 结构中的 shm_nattch 计数器减1
*/
int shmdt (const void *__shmaddr);

/*
控制函数
*/
int shmctl (int __shmid,     // 共享内存标志符
            int __cmd,       // 要采取的动作
                             // IPC_STAT  将shmid_ds中数据设为共享内存的当前关联值
                             // IPC_SET   若有足够权限,把当前关联值设置为shmid_ds中给出的值
                             // IPC_RMID  删除共享内存段
            struct shmid_ds *__buf);   // 包含共享内存模式和访问权限的结构

// 内核为每个共享存储段维护着一个 shmid_ds 结构
struct shmid_ds {
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
    ...
}

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "shm_com.h"

int main(int argc, char const *argv[])
{
    int running = 1;
    void *shared_memory = (void *)0;
    struct shared_use_st *shared_stuff;
    int shmid;

    srand((unsigned int)getpid());

    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1) {
        printf("shmget failed\n");
        exit(EXIT_FAILURE);
    }

    shared_memory = shmat(shmid, (void *)0, 0);
    if (shared_memory == (void *)-1) {
        printf("shmat failed\n");
        eixt(EXIT_FAILURE);
    }

    printf("Memory attached at %X\n", (int)shared_memory);

    shared_stuff = (struct shared_use_st *)shared_memory;
    shared_stuff->written_by_you = 0;
    while (running) {
        if (shared_stuff->written_by_you) {
            printf("You wrote: %s", shared_stuff->some_text);
            sleep(rand() % 4);
            shared_stuff->written_by_you = 0;
            if (strncmp(shared_stuff->some_text, "end", 3) == 0) {
                running = 0;
            }
        }
    }

    if (shmdt(shared_memory) == -1) {
        printf("shmdt failed\n");
        exit(EXIT_FAILURE);
    }

    if (shmctl(shmid, IPC_RMID, 0) == -1) {
        printf("shmctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include "shm_com.h"

int main(int argc, char const *argv[])
{
    int running = 1;
    void *shared_memory = 0;
    struct shared_use_st *shared_stuff;
    char buffer[BUFSIZ];
    int shmid;

    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666 | IPC_CREAT);
    if (shmid == -1) {
        printf("shmget failed\n");
        exit(EXIT_FAILURE);
    }

    shared_memory = shmat(shmid, 0, 0);
    if (shared_memory == (void *)-1) {
        printf("shmat failed\n");
        exit(EXIT_FAILURE);
    }

    printf("Memory attached at %X \n", (int)shared_memory);

    shared_stuff = (struct shared_use_st *)shared_memory;
    while (running) {
        while (shared_stuff->written_by_you == 1) {
            sleep(1);
            printf("waiting for client...\n");
        }
        printf("Enter some text: ");
        fgets(buffer, BUFSIZ, stdin);

        strncpy(shared_stuff->some_text, buffer, TEXT_SZ);
        shared_stuff->written_by_you = 1;

        if (strncmp(buffer, "end", 3) == 0) {
            running = 0;
        }
    }

    if (shmdt(shared_memory) == -1) {
        printf("shmdt failed\n");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);
}

通过文件和mmap共享内存

编译时要指定 -lrt

// 打开指定的文件,后续可以用于mmap调用
extern int shm_open (const char *__name, int __oflag, mode_t __mode);
// oflag 可取 O_RDWR 等,与open函数参数类似

// 将描述符标记为删除,当没有进程使用时释放资源
extern int shm_unlink (const char *__name);

消息队列

在程序间传递数据的简单办法,提供了一种在两个不相关程序间传递数据的办法。它独立于发送和接收程序存在,消除了同步命名管道打开关闭时可能产生的困难。

可以通过发送消息避免命名管道的同步和阻塞问题,可用一些方法提前擦好看紧急消息。但与命名管道一样,每个数据库都有一个最大长度限制,系统中所有队列所包含的全部数据块总长度也有一个上限。

#include <sys/msg.h>

// 发送消息的类型
struct my_message {
    long int message_type;
    /* The data you wish to transfer */
}

/*
创建新队列或打开已有队列
*/
int msgget (key_t __key, 
            int __msgflg); // 由9个权限标志位组成,IPC_CREATE按位与

// 数据复制到队列里,而非直接使用传递的指针
int msgsnd (int __msqid, 
            const void *__msgp, // 准备发送的消息的指针
            size_t __msgsz,     // 消息的长度,不能包括长整型消息类型成员变量的长度
            int __msgflg);      // 队列满或达到系统范围限制时要发生的事
                                //      IPC_NOWAIT 函数立即返回,不发消息且返回-1
// 数据从队中复制出
ssize_t msgrcv (int __msqid, 
                void *__msgp,      // 准备接收消息的指针
                size_t __msgsz,    // 消息的长度,不包括长整型消息类型成员变量的长度
                long int __msgtyp, // 一个长整型,实现一种简单的接收优先级。一般为0
                                   // type==0 返回队列第一个消息
                                   // type>0  返回队列中消息类型为type的第一个消息
                                   // type<0  返回队列中消息类型小于等于type绝对值的消息,如果有若干个,则取类型最小的消息
                int __msgflg);     // 控制没有消息时动作
                                   //     IPC_NOWAIT 立刻返回,没消息时返回值为-1,error设置为ENOMSG。否则挂起


int msgctl (int __msqid, 
            int __cmd,   // 要采取的动作
                         //   IPC_STAT  把 struct msqid_ds 数据设置为消息队列的当前关联值
                         //   IPC_SET   若有足够权限,把消息队列的当前关联值设置为 struct msqid_ds 中给出的值
                         //   IPC_RMID  删除消息队列
            struct msqid_ds *__buf);

/*
每个队列都有一个 msqid_ds 与其关联
*/
struct msqid_ds {
    uid_t msg_perm.uid;
    uid_t msg_perm.gid;
    uid_t msg_perm.mode;
    ...
}

例子:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>

struct my_msg_st {
    long int my_msg_type;
    char some_text[BUFSIZ];
};

int main(int argc, char const *argv[])
{
    int running = 1;
    int msgid;
    struct my_msg_st some_data;
    long int msg_to_reveive = 0;

    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if (msgid == -1) {
        exit(EXIT_FAILURE);
    }

    while (1) {
        if (msgrcv(msgid, (void *)&some_data, BUFSIZ, msg_to_reveive, 0) == -1) {
            exit(EXIT_FAILURE);
        }
        printf("You wrote: %s", some_data.some_text);
        if (strncmp(some_data.some_text, "end", 3) == 0) {
            break;
        }
    }

    if (msgctl(msgid, IPC_RMID, 0) == -1) {
        exit(EXIT_FAILURE);
    }

    return 0;
}
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>

struct my_msg_st {
    long int my_msg_type;
    char some_text[BUFSIZ];
};

int main(int argc, char const *argv[])
{
    int running = 1;
    int msgid;
    struct my_msg_st some_data;
    long int msg_to_reveive = 0;

    msgid = msgget((key_t)1234, 0666 | IPC_CREAT);
    if (msgid == -1) {
        exit(EXIT_FAILURE);
    }

    while (1) {
        if (msgrcv(msgid, (void *)&some_data, BUFSIZ, msg_to_reveive, 0) == -1) {
            exit(EXIT_FAILURE);
        }
        printf("You wrote: %s", some_data.some_text);
        if (strncmp(some_data.some_text, "end", 3) == 0) {
            break;
        }
    }

    if (msgctl(msgid, IPC_RMID, 0) == -1) {
        exit(EXIT_FAILURE);
    }

    return 0;
}

POSIX 信号量




销毁信号量

减1操作


加1操作

未命名信号量,在单个进程中使用,仅仅改变创建和销毁信号量的方式。


posted @ 2022-06-16 09:41  某某人8265  阅读(120)  评论(1编辑  收藏  举报