Linux环境高级编程

参考作者: Kisugi Takumi
链接: https://kisugitakumi.net/2022/11/20/Linux%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#4-2-7-glob
来源: 来生拓己 オフィシャルサイト
著作权归作者所有。仅学习记录使用。

1 进程环境

本节对应第七章——进程环境;

1.1 main函数

C程序总是从main函数开始执行,从main函数结束执行。即main是程序的入口和出口。

当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址———这是由连接编辑器设置的,而连接编辑器则由C编译器调用。

启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。

1.2 进程终止

1.2.1 终止方式

共有8种方式让进程终止。其中5种为正常退出:

  1. 从main返回
  2. 调用exit(C库函数)
  3. 调用_exit_Exit(系统调用)
  4. 最后一个线程从其启动例程返回
  5. 从最后一个线程调用pthread_exit

异常终止有3种方式:

  1. 调用abort
  2. 接到一个信号
  3. 最后一个线程对取消请求做出响应
1.2.2 main函数的返回值
1.2.3 钩子函数atexit

按照ISO C的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序(exit handler),并调用 atexit 函数来登记这些函数。

atexit当程序正常终止时,调用指定的函数(终止处理程序) func。可以在任何地方注册终止函数,但它会在程序终止的时候被调用。先注册的后调用。

func — 在程序终止时被调用的函数,该函数无参且无返回值,它是一个函数指针,因此传入的参数应该是一个函数的地址,即函数名(函数名就是函数的首地址)。
如果函数成功注册,则该函数返回零,否则返回一个非零值。
程序实例

#include <stdio.h>
#include <stdlib.h>

// 终止处理程序
static void f1() {
    puts("f1() is working!");
}

// 终止处理程序
static void f2() {
    puts("f2() is working!");
}

// 终止处理程序
static void f3() {
    puts("f3() is working!");
}

int main() {
    puts("Begin");
    // 先注册的后被调用
    // 钩子函数的书写顺序并不是实际执行顺序,atexit会在程序终止时被调用
    // atexit参数用指针来接收,因此需要传入地址,而函数名就是函数的地址
    atexit(f1);
    atexit(f2);
    atexit(f3);

    puts("End");
    exit(0);
}

执行结果:

Begin
End
f3() is working!
f2() is working!
f1() is working!
1.2.4 exit和_exit

exit是库函数,而_exit是系统调用,前者使用了后者。

除此之外,_exit()执行后会立即返回给内核,而exit()要先执行一些清除和终止操作,然后将控制权交给内核。

1.3 命令行参数

1.4 环境变量


1.5 共享库


1.6 函数间跳转

补充:goto语句

C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。

语法:

goto label;
..
.
label: statement;

在这里,label 可以是任何除 C 关键字以外的纯文本,它可以设置在 C 程序中 goto 语句的前面或者后面。

代码示例:

#include <stdio.h>

int main() {
   /* 局部变量定义 */
   int a = 10;
 
   /* do 循环执行 */
   LOOP:
   do {
      if( a == 15) {
         /* 跳过迭代 */
         a = a + 1;
         goto LOOP;
      }
      printf("a 的值: %d\n", a);
      a++;
   } while(a < 20);
 
   return 0;
}

执行结果:

a 的值: 10
a 的值: 11
a 的值: 12
a 的值: 13
a 的值: 14
a 的值: 16
a 的值: 17
a 的值: 18
a 的值: 19

setjmplongjmp可以实现非局部控制转移,即从一个函数到另外一个函数的跳转

函数原型:

#include <setjmp.h>
int setjmp(jmp_buf buf);
void longjmp(jmp_buf env, int val);
  • setjmp函数用于记录当前位置,保存调用函数的栈环境在结构体jmp_buf buf(相当于保护现场)。函数输入参数为jmp_buf类型(这个结构体类似于goto的跳转标识),返回整型。当第一次调用时(设置跳转点),它的值为0;当第二次调用时(从别处跳转回来,即调用longjmp时)返回非零值;总之执行一次,返回两次,因此,setjmp函数后常常跟上分支语句。
  • longjmp的作用是使用setjmp保存在buf中的栈环境信息返回到setjmp的位置,也就是当执行longjmp时程序又回到setjmp处(相当于恢复现场)。形参val是调用longjmp时setjmp函数返回的值,为非零值,如果故意设置为0,也会被修改为1;

程序实例

#include <stdio.h>
#include <stdlib.h>

static void c(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}

static void b(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call c().\n", __FUNCTION__);
    c();
    printf("%s():c() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}

static void a(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call b().\n", __FUNCTION__);
    b();
    printf("%s():b() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}

int main(void) {

    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call a().\n", __FUNCTION__);
    a();
    printf("%s():a() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
    exit(0);
}

执行结果: 

main():Begin.
main():Call a().
a():Begin.
a():Call b().
b():Begin.
b():Call c().
c():Begin.
c():End.
b():c() returned.
b():End.
a():b() returned.
a():End.
main():a() returned.
main():End.

注:ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

例如:

__DATE__ 当前日期,一个以 “MMM DD YYYY” 格式表示的字符串常量。
__TIME__ 当前时间,一个以 “HH:MM:SS” 格式表示的字符串常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__FUNCTION__ 程序预编译时预编译器将用所在的函数名,返回值是字符串;

现在改写程序,在函数a进行setjmp,函数c进行longjmp

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

// 跳转点的现场环境
static jmp_buf save;

static void c(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Jump now!.\n", __FUNCTION__);
    // 向save跳转,并携带返回值为6
    longjmp(save, 6);
    printf("%s():End.\n", __FUNCTION__);
}

static void b(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call c().\n", __FUNCTION__);
    c();
    printf("%s():c() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}

static void a(void) {
    // 返回值
    int ret;
    printf("%s():Begin.\n", __FUNCTION__);
    // 设置跳转点
    // setjmp一次调用,两次返回
    ret = setjmp(save);
    if(ret == 0) {
        printf("%s():Call b().\n", __FUNCTION__);
        b();
        printf("%s():b() returned.\n", __FUNCTION__);
    } else {
        printf("%s():Jumped back here with code %d.\n", __FUNCTION__, ret);
    }
    printf("%s():End.\n", __FUNCTION__);
}

int main(void) {

    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call a().\n", __FUNCTION__);
    a();
    printf("%s():a() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
    exit(0);
}

 执行结果:

main():Begin.
main():Call a().
a():Begin.
a():Call b().
b():Begin.
b():Call c().
c():Begin.
c():Jump now!.
a():Jumped back here with code 6.
a():End.
main():a() returned.
main():End.


1.7 资源的获取和控制

2 进程控制

该节对应第八章——进程控制。

2.1 进程标识


每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。例如,应用程序有时就把进程 ID 作为名字的一部分来创建一个唯一的文件名。

进程标识符的类型为pid_t,其本质上是一个无符号整型(unsigned int)的类型别名。

进程ID是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数 UNIX 系统实现延迟复用算法,使得赋予新建进程的 ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的先前进程。

系统中有一些专用进程,但具体细节随实现而不同。

  • ID为0的进程通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,它并不执行任何硬盘上的程序,因此也被称为系统进程
  • 进程ID为1通常是 init 进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新版本中是/sbin/init。此进程负责在自举内核后启动一个UNIX系统。init 进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。

常用系统调用

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

// 返回该函数进程的父进程标识符
pid_t getppid(void);

// 返回当前进程标识符
pid_t getpid(void);

补充:ps命令 

Linux 中的 ps 命令是 Process Status 的缩写。ps 命令用来列出系统中当前正在运行的那些进程,就是执行 ps 命令的那个时刻的那些进程的快照。

参数:

参数    含义
-e    显示所有进程
-f    全格式
-l    长格式
a    显示终端上的所有进程,包括其他用户的进程
r    只显示正在运行的进程
x    显示没有控制终端的进程

常用组合:

ps aux # 查看全部进程,以用户为主的格式显示进程情况
lei@ubuntu:~$ ps aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  2.7  0.2 225456  8988 ?        Ss   00:04   0:08 /sbin/init auto noprompt
root          2  0.0  0.0      0     0 ?        S    00:04   0:00 [kthreadd]
root          3  0.0  0.0      0     0 ?        I<   00:04   0:00 [rcu_gp]
root          4  0.0  0.0      0     0 ?        I<   00:04   0:00 [rcu_par_gp]
root          5  0.0  0.0      0     0 ?        I    00:04   0:00 [kworker/0:0-cgr]
root          6  0.0  0.0      0     0 ?        I<   00:04   0:00 [kworker/0:0H-kb]
root          7  0.0  0.0      0     0 ?        I    00:04   0:00 [kworker/u256:0-]
root          8  0.0  0.0      0     0 ?        I<   00:04   0:00 [mm_percpu_wq]
root          9  0.9  0.0      0     0 ?        S    00:04   0:02 [ksoftirqd/0]
root         10  1.0  0.0      0     0 ?        I    00:04   0:03 [rcu_sched]
root         11  0.0  0.0      0     0 ?        S    00:04   0:00 [migration/0]
root         12  0.0  0.0      0     0 ?        S    00:04   0:00 [idle_inject/0]
root         13  0.2  0.0      0     0 ?        I    00:04   0:00 [kworker/0:1-eve]
root         14  0.0  0.0      0     0 ?        S    00:04   0:00 [cpuhp/0]
root         15  0.0  0.0      0     0 ?        S    00:04   0:00 [cpuhp/1]
root         16  0.0  0.0      0     0 ?        S    00:04   0:00 [idle_inject/1]
root         17  0.7  0.0      0     0 ?        S    00:04   0:02 [migration/1]
ps ef # 显示出linux机器所有详细的进程信息
lei@ubuntu:~$ ps ef
   PID TTY      STAT   TIME COMMAND
  2666 pts/0    Ss     0:00 bash XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg LANG=en_US.UTF-8 DISPLAY=:0 SHLVL=0 LOGNAME=lei XDG_VTNR=2 PWD=/home/
  2868 pts/0    R+     0:00  \_ ps ef CLUTTER_IM_MODULE=xim LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;
  2132 tty2     Ssl+   0:00 /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu XDG_SEAT=se
  2134 tty2     Rl+    0:09  \_ /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -background none -noreset -keeptty -verbose
  2143 tty2     Sl+    0:01  \_ /usr/lib/gnome-session/gnome-session-binary --session=ubuntu USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSI
  2262 tty2     Sl+    0:20      \_ /usr/bin/gnome-shell USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION_TYPE=x11 SSH_AGENT_PID=2233 SHLVL
  2328 tty2     Sl     0:00      |   \_ ibus-daemon --xim --panel disable USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION_TYPE=x11 SSH_AGE
  2332 tty2     Sl     0:00      |       \_ /usr/lib/ibus/ibus-dconf USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION_TYPE=x11 SSH_AGENT_PI
  2568 tty2     Sl     0:00      |       \_ /usr/lib/ibus/ibus-engine-simple USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION_TYPE=x11 SSH_
  2401 tty2     Sl+    0:00      \_ /usr/lib/gnome-settings-daemon/gsd-power USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION_TYPE=x11 SSH_
  2403 tty2     Sl+    0:00      \_ /usr/lib/gnome-settings-daemon/gsd-print-notifications USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION
  2406 tty2     Sl+    0:00      \_ /usr/lib/gnome-settings-daemon/gsd-rfkill USER=lei TEXTDOMAIN=im-config XDG_SEAT=seat0 XDG_SESSION_TYPE=x11 SSH
  2410 tty2     Sl+    0:00      \_ /usr/lib/gnome-settings-daemon/gsd-screensaver-proxy 
ps aux | grep bash  
lei@ubuntu:~$ ps aux | grep bash
lei        2666  0.0  0.1  24204  4936 pts/0    Ss   00:08   0:00 bash
lei        2870  0.0  0.0  16184  1080 pts/0    S+   00:12   0:00 grep --color=auto bash

2.2 进程产生

2.2.1 fork

init进程:pid为1,是所有进程的祖先进程,注意不是父进程。

一个现有的进程可以调用fork函数创建一个新进程:

#include <unistd.h>

pid_t fork(void);

返回值:fork函数被调用一次,但返回两次。
       子进程的返回值是0,父进程的返回值则是新建子进程的进程PID。
       如果失败则返回-1,并设置errno。和setjmp类似,fork语句后常常跟上分支语句进行判断。

由fork创建的新进程被称为子进程(child process)

子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆、栈和缓冲区和文件描述符的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分,除了读时共享的部分。

fork后父子进程的区别

  • fork返回值不同
  • 两个进程的pid不同
  • 两个进程的ppid也不同,父进程的ppid是它的父进程pid,而子进程的ppid是创建它的进程的pid
  • 父进程的未决信号和文件锁不继承
  • 子进程的资源利用量归零

程序实例1——fork的使用

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

int main(void) {
    pid_t pid;
    printf("[%d]:Begin!\n", getpid());
    pid = fork();
    // ---------------------------
    // 父进程调用fork后,父子进程都从这里开始执行
    if(pid < 0) {
        perror("fork()");
        exit(1);
    }
    
    if(pid == 0) { // 如果返回值pid为0,则为子进程
        printf("[%d]:Child is working!\n", getpid());
    } else { // 如果返回值pid大于0,则为父进程
        printf("[%d]:Parent is working!\n", getpid());
    }

    printf("[%d]:End!\n", getpid());
    exit(0);
}

运行结果:(可能结果)

lei@ubuntu:~/Desktop$ ./a.out 
[3034]:Begin!
[3034]:Parent is working!
[3034]:End!
[3035]:Child is working!
[3035]:End!

一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法

如果在main程序返回前添加一行:

getchar();

使得父子进程都暂停,再使用命令:

ps axf

可以看到两个进程与bash的关系如下:(3050父进程,3051子进程)

  2666 pts/0    Ss     0:00  |   \_ bash
  2929 pts/0    T      0:00  |   |   \_ vim 1.c
  3050 pts/0    S+     0:00  |   |   \_ ./a.out
  3051 pts/0    S+     0:00  |   |       \_ ./a.out

程序实例2——fflush的重要性

对于上述程序的结果,注意到Begin只打印了一次:

lei@ubuntu:~/Desktop$ ./a.out 
[3034]:Begin!
[3034]:Parent is working!
[3034]:End!
[3035]:Child is working!
[3035]:End!

如果将该打印信息重定向至某个文件:

lei@ubuntu:~/Desktop$ ./a.out > /tmp/out

再查看该文件的内容:

lei@ubuntu:~/Desktop$ cat /tmp/out
[3138]:Begin!
[3138]:Parent is working!
[3138]:End!
[3138]:Begin!
[3139]:Child is working!
[3139]:End!

注意到Begin打印了两次。

原因:对于重定向至文件,采用的是全缓冲(除标准输出和标准错误输出),只有进程结束或者缓冲满的时候才刷新缓冲区(遇到换行符不刷新),将缓冲区的内容写入到文件。因此,父进程fork时,尚未刷新缓冲区,因此缓冲区的内容[18060]:Begin!,被复制到子进程的缓冲区中,当父子进程执行结束时,强制刷新,输出两次[18060]:Begin!

为防止缓冲区内容被复制,父进程在fork之前需要强制刷新所有已经打开的流:

fflush(NULL);

这样,就只打印一句begin:

lei@ubuntu:~/Desktop$ cat /tmp/out
[3184]:Begin!
[3184]:Parent is working!
[3184]:End!
[3185]:Child is working!
[3185]:End!

程序实例3——找质数

  • 单进程版:
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    #define LEFT 30000000
    #define RIGHT 30000200
    
    int main(void) {
        int i, j, mark;
        for(i = LEFT; i <= RIGHT; i++) {
            mark = 1;
            for(j = 2; j < i/2; j++) {
                if(i % j == 0) {
                    mark = 0;
                    break;
                }
            }
            if(mark)
                printf("%d is a primer.\n", i);
        }
        exit(0);
    }
    lei@ubuntu:~/Desktop$ time ./a.out 
    30000001 is a primer.
    30000023 is a primer.
    30000037 is a primer.
    30000041 is a primer.
    30000049 is a primer.
    30000059 is a primer.
    30000071 is a primer.
    30000079 is a primer.
    30000083 is a primer.
    30000109 is a primer.
    30000133 is a primer.
    30000137 is a primer.
    30000149 is a primer.
    30000163 is a primer.
    30000167 is a primer.
    30000169 is a primer.
    30000193 is a primer.
    30000199 is a primer.
    
    real	0m2.061s
    user	0m2.039s
    sys	0m0.021s
    
  • 多进程版:
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    
    #define LEFT 30000000
    #define RIGHT 30000200
    
    int main(void) {
        int i, j, mark;
        pid_t pid;
        for(i = LEFT; i <= RIGHT; i++) {
            pid = fork();
            if(pid < 0) {
                perror("fork()");
                exit(1);
            }
            if(pid == 0) { // child
                mark = 1;
                for(j = 2; j < i/2; j++) {
                    if(i % j == 0) {
                        mark = 0;
                        break;
                    }
                }
    
                if(mark)
                    printf("%d is a primer.\n", i);
                // 子进程退出
                exit(0);
            }
        }
        exit(0);
    }
    
    
    lei@ubuntu:~/Desktop$ time ./a.out 
    
    real	0m0.215s
    user	0m0.000s
    sys	0m0.055s
    lei@ubuntu:~/Desktop$ 30000037 is a primer.
    30000133 is a primer.
    30000001 is a primer.
    30000041 is a primer.
    30000023 is a primer.
    30000071 is a primer.
    30000167 is a primer.
    30000169 is a primer.
    30000193 is a primer.
    30000049 is a primer.
    30000059 is a primer.
    30000079 is a primer.
    30000149 is a primer.
    30000083 is a primer.
    30000109 is a primer.
    30000137 is a primer.
    30000163 is a primer.
    30000199 is a primer.
    

程序实例3——孤儿进程和僵尸进程

修改1:在子进程在退出前,先睡眠1000s,这样父进程会先执行完毕而退出。

int main(void) {
    int i, j, mark;
    pid_t pid;
    for(i = LEFT; i <= RIGHT; i++) {
        pid = fork();
        if(pid < 0) {
            perror("fork()");
            exit(1);
        }
        if(pid == 0) {
            mark = 1;
            for(j = 2; j < i/2; j++) {
                if(i % j == 0) {
                    mark = 0;
                    break;
                }
            }
            if(mark)
                printf("%d is a primer.\n", i);
            // 子进程睡眠1000s
            sleep(1000);
            exit(0);
        }
    }
    exit(0);
}

再使用命令ps axf查看:

  4077 pts/0    S      0:00  \_ ./a.out
  4078 pts/0    S      0:00  \_ ./a.out
  4079 pts/0    S      0:00  \_ ./a.out
  4080 pts/0    S      0:00  \_ ./a.out
  4081 pts/0    S      0:00  \_ ./a.out
  4082 pts/0    S      0:00  \_ ./a.out
  4083 pts/0    S      0:00  \_ ./a.out
  4084 pts/0    S      0:00  \_ ./a.out
  4085 pts/0    S      0:00  \_ ./a.out
  4086 pts/0    S      0:00  \_ ./a.out
  4087 pts/0    S      0:00  \_ ./a.out
  4088 pts/0    S      0:00  \_ ./a.out
  4089 pts/0    S      0:00  \_ ./a.out
  4090 pts/0    S      0:00  \_ ./a.out
  4091 pts/0    S      0:00  \_ ./a.out
  4092 pts/0    S      0:00  \_ ./a.out
  4093 pts/0    S      0:00  \_ ./a.out
  4094 pts/0    S      0:00  \_ ./a.out
  4095 pts/0    S      0:00  \_ ./a.out
  4096 pts/0    S      0:00  \_ ./a.out
  4097 pts/0    S      0:00  \_ ./a.out
  4098 pts/0    S      0:00  \_ ./a.out
  4099 pts/0    S      0:00  \_ ./a.out
  4100 pts/0    S      0:00  \_ ./a.out
  4101 pts/0    S      0:00  \_ ./a.out
  4102 pts/0    S      0:00  \_ ./a.out

此时201个子进程的状态为S(可中断的睡眠状态),且父进程为init进程(每个进程以顶格形式出现)。这里的子进程在init进程接管之前就是孤儿进程

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程所收养,并由 init 进程对它们完成状态收集工作,孤儿进程并不会有什么危害。

修改2:在父进程退出之前,先休眠1000s,再查看进程状态。

int main(void) {
    int i, j, mark;
    pid_t pid;
    for(i = LEFT; i <= RIGHT; i++) {
        pid = fork();
        if(pid < 0) {
            perror("fork()");
            exit(1);
        }
        if(pid == 0) {
            mark = 1;
            for(j = 2; j < i/2; j++) {
                if(i % j == 0) {
                    mark = 0;
                    break;
                }
            }
            if(mark)
                printf("%d is a primer.\n", i);
            exit(0);
        }
    }
    // 父进程睡眠1000s再退出
    sleep(1000);
    exit(0);
}

执行结果:

  2666 pts/0    Ss     0:00  |   \_ bash
  4602 pts/0    S+     0:00  |   |   \_ ./a.out
  4603 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4604 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4605 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4606 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4607 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4608 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4609 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4610 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4611 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4612 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>
  4613 pts/0    Z+     0:00  |   |       \_ [a.out] <defunct>

可以看到子进程状态为Z,即为僵尸状态

僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息(收尸),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

僵尸进程虽然不占有任何内存空间,但如果父进程不调用 wait() / waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害。

避免产生僵尸进程的方式

父子进程之间的文件共享

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。我们说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项。

考虑下述情况,一个进程具有3个不同的打开文件,它们是标准输入、标准输出和标准错误。在从fork返回时,我们有了如图8-2中所示的结构。

重要的一点是,父进程和子进程共享同一个文件偏移量

考虑下述情况:一个进程 fork 了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已重定向(很可能是由 shell 实现的),那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。在这个例子中,当父进程等待子进程时,子进程写到标准输出:而在子进程终止后,父进程也写到标准输出上,并且知道其输出会追加在子进程所写数据之后。如果父进程和子进程不共享同一文件偏移量,要实现这种形式的交互就要困难得多,可能需要父进程显式地动作。

如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。

在fork之后处理文件描述符有以下两种常见的情况:

  1. 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
  2. 父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
2.2.2 vfork

考虑这样一个场景,父进程使用了一个占用内存很大的数据,此时它fork了一个子进程,而子进程仅仅打印一个字符串就退出了,此时这块很大的数据复制到子进程的内存空间中,造成了很大的内存浪费。

为了解决这个问题,在fork实现中,增加了读时共享,写时复制(Copy-On-Write,COW)的机制。写时复制可以避免拷贝大量根本就不会使用的数据(地址空间包含的数据多达数十兆)。因此可以看出写时复制极大提升了Linux系统下fork函数运行的性能。

写时复制指的是子进程的页表项指向与父进程相同的物理页,这也只需要拷贝父进程的页表项就可以了,不会复制整个内存地址空间,同时把这些页表项标记为只读。

  • 读时共享:如果父子进行都不对页面进行操作或只读,那么便一直共享同一份物理页面。
  • 写时复制:只要父子进程有一个尝试进行修改某一个页面(写时),那么就会发生缺页异常。那么内核便会为该页面创建一个新的物理页面,并将内容复制到新的物理页面中,让父子进程真正地各自拥有自己的物理内存页面,并将页表中相应地页表项标记为可写。

在fork还没实现copy on write之前。Unix设计者很关心fork之后立刻执行exec所造成的地址空间浪费,所以引入了vfork系统调用。而现在vfork已经不常用了。

vfork和fork的区别/联系:

  • vfork函数和 fork函数一样都是在已有的进程中创建一个新的进程,但它们创建的子进程是有区别的。
  • 父子进程的执行顺序

    fork: 父子进程的执行次序不确定。
    vfork:保证子进程先运行,在它调用 exec/exit之后,父进程才执形

  • 是否拷贝父进程的地址空间

    fork: 子进程写时拷贝父进程的地址空间,子进程是父进程的一个复制
    vfork:子进程共享父进程的地址空间

  • 调用vfork函数,是为了执行exec函数;如果子进程没有调用 exec/exit,程序会出错

2.3 wait和waitpid

wait系统调用:等待进程改变状态。

进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

wait函数原型如下:

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

pid_t wait(int *status);

status用来保存子进程退出时的一些状态。如果不在意子进程的退出状态,可以设定status为NULL。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出,并存入其中。
可以使用下列的宏函数来处理status:
    WIFEXITED(status):用来指出子进程是否为正常退出,如果是,则会返回一个非零值。
    WEXITSTATUS(status):当WIFEXITED返回非零值时,可以用这个宏来提取子进程的返回值。
如果执行成功,wait会返回子进程的PID;如果没有子进程,则wait返回-1。

代码示例1

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

int main (void) {
    pid_t pid, pret;
    pid = fork();

    if(pid < 0) {
        printf ("error ocurred!\n");
    }else if (pid == 0) { /* 如果是子进程 */
        printf("This is child process with pid of %d\n", getpid());
        sleep (10); /* 睡眠10秒钟 */
	    printf("child is end!\n");
    }else { /* 如果是父进程 */
        pret = wait(NULL); /* 在这里阻塞,收尸 */
        printf ("I catched a child process with pid of %d\n", pret);
    }
    exit(0);
}

打印结果:

lei@ubuntu:~/Desktop$ ./a.out 
This is child process with pid of 4867
child is end!
I catched a child process with pid of 4867

代码示例2

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

int main(void) {
    int status;
    pid_t pid, pr;
    pid = fork();

    if(pid < 0) {
        printf( "error ocurred!\n" );
    }
    if(pid == 0) { /* 子进程 */
        printf("This is child process with pid of %d\n", getpid());
        exit(3); /* 子进程返回3 */
    }
    if(pid > 0) { /* 父进程 */
        pr = wait(&status);
        if(WIFEXITED(status)) { /* 如果WIFEXITED返回非零值 */
            printf("the child process %d exit normally\n", pr);
            printf("the return code is %d\n", WEXITSTATUS(status));
        } else { /* 如果WIFEXITED返回零 */
            printf("the child process %d exit abnormally\n", pr);
        }
    }
    exit(0);
}

 打印结果:

lei@ubuntu:~/Desktop$ ./a.out 
This is child process with pid of 4907
the child process 4907 exit normally
the return code is 3

waitpid函数原型如下:

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

pid_t waitpid(pid_t pid, int *status, int options);

// 下面两者等价:
wait(&status);
waitpid(-1, &status, 0);

从本质上讲,waitpidwait的作用是完全相同的,但waitpid多出了两个可以由用户控制的参数pidoptions

  • pid:当pid取不同的值时,在这里有不同的意义:
    取值意义
    > 0只等待进程ID等于pid的子进程
    == 0等待任何一个子进程退出,此时waitpid和wait的作用一模一样
    == -1等待同一个进程组process group id中的任何子进程
    < -1等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值
  • options:是一个位图,可以通过按位或来设置,如果不设置则置为0即可。最常用的选项是WNOHANG,作用是即使没有子进程退出,它也会立即返回,此时waitpid不同于wait,它变成了非阻塞的函数。

waitpid的返回值有如下几种情况:

  • 当正常返回时,waitpid返回子进程的PID。
  • 如果设置了WNOHANG,而waitpid没有发现已经退出的子进程,则返回0。
  • 如果waitpid出错,则返回-1。例如参数pid指示的子进程不存在,或此进程存在,但不是调用进程的子进程。

代码示例1

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define LEFT 30000000
#define RIGHT 30000200

int main(void) {
    int i, j, mark, status;
    pid_t pid;
    pid_t pid_child;
    for(i = LEFT; i <= RIGHT; i++) {
        pid = fork();
        if(pid < 0) {
            perror("fork()");
            exit(1);
        }
        if(pid == 0) {
            mark = 1;
            for(j = 2; j < i/2; j++) {
                if(i % j == 0) {
                    mark = 0;
                    break;
                }
            }

            if(mark)
                printf("[%d]:%d is a primer.\n", getpid(), i);
            exit(3);
        }
    }
    // 循环201次,给201个子进程收尸
    for(i = LEFT; i <= RIGHT; i++) {
        pid_child = waitpid(-1, &status, 0);
    
        if(WIFEXITED(status)) { /* 如果WIFEXITED返回非零值 */
            printf("the return code is %d\n", WEXITSTATUS(status ));
        } else { /* 如果WIFEXITED返回零 */
            printf("the child process exit abnormally\n");
        }

        printf("Child process with pid: %d.\n", pid_child);
    }
    exit(0);
}

执行结果:

the return code is 3
Child process with pid: 3516.
the return code is 3
Child process with pid: 3518.
[3377]:30000059 is a primer.
the return code is 3
Child process with pid: 3377.
[3487]:30000169 is a primer.
the return code is 3
Child process with pid: 3487.
[3319]:30000001 is a primer.
the return code is 3
Child process with pid: 3319.

2.4 exec函数族

2.4.1 简介

fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。

当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

为什么需要exec函数

  • fork子进程是为了执行新程序(fork创建了子进程后,子进程和父进程同时被OS调度执行,因此子进程可以单独的执行一个程序,这个程序宏观上将会和父进程程序同时进行);
  • 可以直接在子进程的if中写入新程序的代码(参见2.2.1节的做法)。这样可以,但是不够灵活,因为我们只能把子进程程序的源代码贴过来执行(必须知道源代码,而且源代码太长了也不好控制),譬如说我们希望子进程来执行ls -la命令就不行了(没有源代码,只有编译好的可执行程序/usr/bin/ls);
  • 使用exec族运行新的可执行程序(exec族函数可以直接把一个编译好的可执行程序直接加载运行);
  • 我们有了exec族函数后,典型的父子进程程序是这样的:子进程需要运行的程序被单独编写、单独编译连接成一个可执行程序(叫hello),(项目是一个多进程项目)主程序为父进程,fork创建了子进程后在子进程中exec来执行hello,达到父子进程分别做不同程序同时(宏观上)运行的效果;
2.4.2 使用

有多种不同的exec函数可供使用,它们常常被统称为exec函数。

#include <unistd.h>

extern char **environ;

// 直达
int execl(const char *path, const char *arg, ...);
// 从$PATH里找
int execlp(const char *file, const char *arg, ...);
// 直达
int execle(const char *path, const char *arg, ..., char * const envp[]);

// 直达
int execv(const char *path, char *const argv[]);
// 从$PATH里找
int execvp(const char *file, char *const argv[]);
// 从$PATH里找
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 以上函数成功执行时不返回,失败时返回-1并设值errno

后缀含义

l:以list形式传入参数
v:以vector形式传入参数
p:在$PATH中查找可执行程序
e:在envp[]中查找可执行程序

  • execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成
  • execv是把参数列表事先放入一个字符串数组中(必须以NULL结尾),再把这个字符串数组传给execv函数,类似于char **argv
  • path:完整的文件目录路径
  • file:文件名,系统就会自动从环境变量$PATH所指出的路径中进行查找该文件。如果包含/,则视为路径名path。

其中execve是内核的系统调用,其他函数皆为库函数。

代码示例——环境变量

  • myexec.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(void) {
        
        pid_t pid;
        pid = fork();
        if(pid < 0) {
            perror("fork()");
            exit(1);
        } else if(pid == 0) { // 子进程
            // 参数
            char * const param[] = {"myHello", "-l", "-a",  NULL};
            // 自己设置的环境变量
            char * const envp[] = {"123456", "abc", "AA=aaa", "BB=bbb", NULL};
            // 执行同目录下的hello
            execvp("./hello", param, envp);
            perror("execvpe()");
            exit(1);
        } else { // 父进程
            wait(NULL);
        }
        exit(0);
    }
  • hello.c
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char **argv, char **env)
    {
        int i = 0;
    
        printf("argc = %d\n", argc);
    
        for (i = 0; argv[i] != NULL; i++)
        {
            printf("argv[%d]: %s\n", i, argv[i]);
        }
    
        for (i = 0; env[i] != NULL; i++)
        {
            printf("env[%d]: %s\n", i, env[i]);
        }
    
        return 0;
    }

编译链接为hello

执行结果:

lei@ubuntu:~/Desktop$ ./a.out 
argc = 3
argv[0]: myHello
argv[1]: -l
argv[2]: -a
env[0]: 123456
env[1]: abc
env[2]: AA=aaa
env[3]: BB=bbb

(疑问:为什么argc记录的是argv的个数?)

2.5 shell外部命令实现

2.6 用户权限和组权限

2.7 解释器文件

2.8 system函数

函数原型:

#include <stdlib.h>

int system(const char *command);

作用:该函数实际上调用的是/bin/sh -c command,实质上是对fork+exec+wait的封装。

程序实例 

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    
    system("date +%s > /tmp/out");
    
    exit(0);
}

 该程序实质上执行的命令为:

/bin/sh -c date +%s > /tmp/out

 该程序实质上执行的命令为:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    pid_t pid;
    
    pid = fork();
    if(pid < 0) {
        perror("fork()");
        exit(1);
    }
    
    if(pid == 0) {
        // 实际上在 exec /bin/sh程序
        execl("/bin/sh", "sh", "-c", "date +%s > /tmp/out", NULL);
        perror("execl()");
        exit(1);
    }
    
    wait(NULL);
    exit(0);
}

2.9 守护进程

3 信号

该节对应第十章——信号。

3.1 前置概念

并行和并发

同步和异步 

3.2 信号的概念

信号是一种软中断,进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断,从它的命名可以看出,它的实质和使用很像中断。信号提供了一种处理异步事件的方法。

进程之间可以通过调用kill库函数发送软中断信号。Linux内核也可能给进程发送信号,通知进程发生了某个事件(例如内存越界)。

每个信号都有一个名字。这些名字都以3个字符SIG开头。头文件<signal.h>中,信号名都被定义为正整数常量(信号编号)。

通过命令kill -l可以列出所有可用信号:

信号值 1 ~ 31 为不可靠信号(标准信号),信号会丢失;信号值 34 ~ 64 为可靠信号(实时信号),信号不会丢失。

lei@ubuntu:~$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如 errno)来判断是否发生了一个信号,而是必须告诉内核”在此信号发生时,请执行下列操作”。

当某个信号出现时,可以告诉内核按下列三种方式之一进行处理,称之为信号的处理或与信号相关的工作:

  • 忽略此信号
  • 捕捉信号
  • 执行系统默认工作

下图列出了所有信号的名字,说明了哪些系统支持此信号和信号对应的系统默认工作。可以看出C标准库支持的信号是最少的。大部分的信号的默认操作是终止进程。

在系统默认动作列,“终止+core”表示在进程当前工作目录的core文件中复制了该进程的内存映像,core文件记录了进程终止时的错误报告,大多数UNIX系统调试程序都使用core文件检查进程终止时的状态。

补充:kill命令

kill [参数] [进程号]

常用参数:

  • 如果是kill -l,则列出全部的信号名称
  • 如果是kill -信号编号 pid,则将该编号对应的信号发送给指定pid的进程
  • 默认为15,对应发出终止信号,例如kill 23007

3.3 signal函数

 UNIX系统信号机制最简单的接口是signal函数,函数原型如下:

// CONFORMING TO C89, C99, POSIX.1-2001.
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

// 注意,typedef没有定义在头文件中,因此必须要写出,否则按照下面的形式给出signal函数,APUE上的就是这种形式:

void (* signal(int signum, void (*func)(int)))(int);

作用:
    signal函数为signum所代表的信号设置一个信号处理程序func,换句话说,signal就是一个注册函数。

参数:
    signum参数是上图中的信号名,常用宏名来表示,例如SIGINT
    func参数是下面的一种:
        SIG_IGN:向内核忽略此信号,除了SIGKILL和SIGSTOP
        SIG_DFL:执行系统默认动作
        当接到此信号后要调用的函数的地址:在信号发生时,调用该函数;
             称这种处理为捕捉该信号,称此函数为信号处理程序或信号捕捉函数
返回值:
    成功返回以前的信号处理配置
    失败返回 SIG_ERR

代码示例1

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <unistd.h>

int main()
{
    int i = 0;
    //忽略SIGINT信号(Ctrl+c)
    signal(SIGINT, SIG_IGN);

    for (i = 0; i < 10; i++)
    {
        //每秒向终端打印一个*
        write(1, "*", 1);
        sleep(1);
    }
        
    return 0;
}

 信号SIGINT产生的方式就是快捷键CTRL + C。

执行结果:

lei@ubuntu:~/Desktop$ ./a.out 
***^C*^C*^C*^C*^C*^C^C^C^C^C**lei@ubuntu:~/Desktop$ 

代码示例2

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <unistd.h>

static void sig_handler(int a)
{
    //向终端打印!
    write(1, "!", 1);

    return;
}

int main()
{
    int i = 0;
    //函数名就是函数的地址
    signal(SIGINT, sig_handler);

    for (i = 0; i < 10; i++)
    {
        //每秒向终端打印一个*
        write(1, "*", 1);
        sleep(1);
    }
        
    return 0;
}

执行结果: 

lei@ubuntu:~/Desktop$ ./a.out 
***^C!**^C!*^C!*^C!*^C!*^C!*^C!lei@ubuntu:~/Desktop$ 

代码示例3——阻塞和非阻塞

上述程序,如果一直按着CTRL + C,程序会小于10S就会结束。

原因在于:信号会打断阻塞的系统调用。这里的阻塞是writesleep函数。

分析:进程运行到sleep(1)的时候,由运行态进入阻塞态,此时如果有信号到来,例如SIGINT,会打断阻塞(唤醒进程),让进程进入就绪态,获得时间片进入运行态,此时进程还没阻塞到1s,就进入了就绪态,即信号会打断阻塞的系统调用。

  • 阻塞:为了完成一个功能,发起一个调用,如果不具备条件的话则一直等待,直到具备条件则完成
  • 非阻塞:为了完成一个功能,发起一个调用,具备条件直接输出,不具备条件直接报错返回

此前学习过的所有IO函数,都是阻塞IO,即阻塞的系统调用。

以open为例,进程调用open时,进入阻塞态,等待IO设备打开,如果IO设备打开时间过长,此时有一个信号到来,就会打断open调用,使其打开设备失败。

因此,在设备打开失败的时候,需要判断是因为open自身引发的错误,还是因为信号打断而没有打开,对于前者,以以往的方式处理错误,而对于后者应该尝试再次打开设备,而不是报错后退出程序。

注意:对于所有的阻塞系统调用,都要处理是因为自身调用出现的真错,还是因为信号中断导致的假错。

在宏中,有一个名为EINTRerrno,即为被信号中断而引发的错误。当进程在执行一个阻塞的系统调用时捕捉到一个信号,则被中断不再执行该系统调用,该系统调用返回错误,errno就会被设置为EINTR

  EINTR  While  blocked  waiting  to  complete  an  open of a slow device
              (e.g., a FIFO; see fifo(7)), the call was interrupted by a  sig‐
              nal handler; see signal(7).
(来自 man 2 open)
  EINTR  The call was interrupted by a signal before any data  was  read;
              see signal(7).
 (来自 man 2 read)

以前面的一个程序为例,修改后的代码为:

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

int main(int argc, char const *argv[])
{
    int fd1 = 0;
    int fd2 = 0;
    ssize_t nret = 0;
    char buff[4096] = {0};

   do {
        fd1 = open("1.jpg", O_RDONLY, 0664);
        if (-1 == fd1)//真错,退出
        {
            perror("fail to open fd1");
            return -1;
        }
        if (errno == EINTR)//假错,重新打开
        {
            continue;
        }
    }while(fd1 < 0);
    
    do {
        fd2 = open("2.jpg", O_WRONLY | O_TRUNC | O_CREAT, 0664);
        if (-1 == fd2)
        {
            perror("fail to open fd2");
            return -1;
        }
        if (errno == EINTR)//假错,重新打开
        {
            continue;
        }
    }while(fd2 < 0);
    

    while(1)
    {
        nret = read(fd1, buff, sizeof(buff));
        if (nret <= 0)
        {
            if (errno == EINTR)
                continue;
            break;
        }
        nret = write(fd2, buff, nret);
        if (nret <= 0)
        {
            if (errno == EINTR)
                continue;
            break;
        }
        printf("nret = %ld\n", nret);
        /*
        if (nret < sizeof(buff))
        {
            break;
        }
        */
    }

    close(fd1);
    close(fd2);

    return 0;
}

3.4 不可靠的信号

信号处理程序由内核调用,在执行该程序时,内核为该处理程序布置现场,此时如果又来一个信号,内核再次调用信号处理程序,可能会冲掉第一次调用布置的现场。

3.5 可重入函数

3.6 信号的响应过程

3.7 常用函数Ⅰ

3.7.1 kill

kill函数用于向进程发送信号,注意不是杀死进程。

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

int kill(pid_t pid, int sig);

参数:

  • pid:向哪个进程发送信号

        pid > 0:发送信号给指定进程
        pid = 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程,相当于组内广播。
        pid < -1:发送信号给该绝对值所对应的进程组id的所有组员,相当于组内广播。
        pid = -1:发送信号给所有权限发送的所有进程。

  • sig:待发送的信号

        sig = 0:没有信号发送(空信号),但会返回-1并设置errno,用来检测某个进程id或进程组id是否存在。注意返回-1时并不能表明该id不存在,而是要根据errno来判断,详见下面的返回值。

返回值:

  • 成功返回0
  • 失败返回-1,并设值errno

        EINVAL:无效的信号sig
        EPERM:调用进程没有权限给pid的进程发送信号
        ESRCH:进程或进程组不存在

3.7.2 raise

raise函数用于向自身发送信号,即自己给自己发送信号。

#include <signal.h>

int raise(int sig);

// 相当于
kill(getpid(), sig);
3.7.3 alarm
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

作用:设置定时器。在指定seconds后,内核会给当前进程发送SIGALRM信号(定时器超时)。进程收到该信号,默认动作是终止调用该alarm函数的进程。每个进程都有且只有唯一的一个定时器,所以多个alarm函数共同调用时,后面设置的时钟会覆盖掉前面的时钟。

返回值:返回0或剩余的秒数,无失败。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main()
{
    alarm(5);
    alarm(2);
    alarm(3);
    while(1);

    exit(0);
}

执行结果:定时三秒,执行默认动作终止进程。

lei@ubuntu:~/Desktop/sig$ ./a.out 
Alarm clock
3.7.4 pause

pause函数用于等待信号。

#include <unistd.h>

int pause(void);

返回值:
    -1,errno设置为EINTR

进程调用pause函数时,会造成进程主动挂起(处于阻塞状态,并主动放弃CPU),并且等待信号将其唤醒。

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    alarm(5);
    while(1)
        pause();
    exit(0);
}

当调用到pause()时,该进程挂起,此时不再占用CPU,5s过后,接收到SIGALRM信号,采取默认动作终止。

信号的处理方式有三种:

  • 默认动作
  • 忽略处理
  • 捕捉

进程收到一个信号后,会先处理响应信号,再唤醒pause函数。于是有下面几种情况:

  • 如果信号的默认处理动作是终止进程,则进程将被终止,也就是说一收到信号进程就终止了,pause函数根本就没有机会返回,例如上面的例子
  • 如果信号的默认处理动作是忽略,则进程将直接忽略该信号,相当于没收到这个信号,进程继续处于挂起状态,pause函数不返回
  • 如果信号的处理动作是捕捉,则进程调用完信号处理函数之后,pause返回-1,errno设置为EINTR,表示“被信号中断”
  • pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒

sleep = alarm + pause

代码示例

需求:让程序等待5s

  • 使用time
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    time_t end;
    int64_t count = 0;

    end = time(NULL) + 5;

    while(time(NULL) <= end) {
        count++;
    }

    printf("%lld\n", count);

    exit(0);
}

7.8 信号集


信号集是一个能够表示多个信号的数据类型。

POSIX.1定义数据类型sigset_t来表示一个信号集(本质为整型),并且定义了下列5个处理信号集的函数:

#include <signal.h>

// 置空一个信号集
int sigemptyset(sigset_t *set);

// 填充满一个信号集
int sigfillset(sigset_t *set);

// 将一个信号加入信号集
int sigaddset(sigset_t *set, int signum);

// 将一个信号从信号集删除
int sigdelset(sigset_t *set, int signum);

// 上述函数成功返回0,失败返回-1

// 检查一个信号集中是否有这个信号,存在返回1,不存在返回0
int sigismember(const sigset_t *set, int signum);


 

7.9 sigprocmask函数


sigprocmask函数可以检测或更改,或同时进行检测和更改进程的信号屏蔽字(阻塞信号集)。注:对信号来说,阻塞和屏蔽是一个意思。

在所有的信号中,有两个信号比较特殊:SIGKILL -9 SIGSTOP -19,这两个信号不可被阻塞,不可被自定义,不可被忽略。

函数原型:

#include <signal.h>
// 成功返回0,失败返回-1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);


how:用于指定信号修改的方式,有三种选择:

  • SIG_BLOCK:该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。即set包含了希望阻塞的附加信号;
  • SIG_UNBLOCK:该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集。即set包含了希望解除阻塞的信号;
  • SIG_SETMASK:该进程新的信号屏蔽是set指向的值;

*set:和how结合使用
*oldset:进程的当前信号屏蔽字通过oset返回(oldset),如果不关心旧的信号集设置,可设置为NULL

代码示例1

#include <stdio.h>
#include <signal.h>

static void sighandler(int s)
{
    printf("recv signal:%d\n", s); 

    return;
}

int main()
{
    sigset_t set, old;
    
    //注册函数
    signal(SIGINT, sighandler);
    signal(SIGRTMIN+5, sighandler);

    /*清空信号集*/
    sigemptyset(&set);
    sigemptyset(&old);
    /*填满信号集*/
    sigfillset(&set);

    sigprocmask(SIG_BLOCK, &set, &old);//阻塞set里的信号,将set里的信号
保存在old中
    printf("按回车,继续运行!\n");

    getchar();

    //sigpromask(SIG_UNBLOCK, &set, NULL);//解除阻塞
    sigprocmask(SIG_SETMASK, &old, NULL);//解除阻塞

    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/sig$ ./a.out 
按回车,继续运行!

程序阻塞在getchar(),输入多个2号信号(ctrl+c) 程序没有反应,是因为所有信号被屏蔽。

lei@ubuntu:~/Desktop/sig$ ./a.out 
按回车,继续运行!
^C^C^C^C^C^C^C^C^C^C^C^C

打开一个新的终端,使用39号信号给进程发送6次信号。

lei@ubuntu:~$ ps -ef | grep ./a.out
lei        3459   3269  0 01:06 pts/0    00:00:00 ./a.out
lei        3464   3438  0 01:08 pts/1    00:00:00 grep --color=auto ./a.out
lei@ubuntu:~$ kill -39 3459
lei@ubuntu:~$ kill -39 3459
lei@ubuntu:~$ kill -39 3459
lei@ubuntu:~$ kill -39 3459
lei@ubuntu:~$ kill -39 3459
lei@ubuntu:~$ kill -39 3459
lei@ubuntu:~$ 

按下回车。 

lei@ubuntu:~/Desktop/sig$ ./a.out 
按回车,继续运行!
^C^C^C^C^C^C^C^C^C^C^C^C
recv signal:39
recv signal:39
recv signal:39
recv signal:39
recv signal:39
recv signal:39
recv signal:2
lei@ubuntu:~/Desktop/sig$ 

进程处理了6次39号信号,只处理了1次2号信号。

原因是,39号是实时信号,2号是标准信号,实时信号先处理。2号只注册了一次(最后一次)。

4 线程

本节对应APUE第十一、十二章内容

1.1 线程概念

线程本质:一个正在运行的函数。

进程本质:加载到内存的程序。

进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源。

典型的UNIX进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计成在某一时刻能够做不止一件事,每个线程处理各自独立的任务。

每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。

一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码、程序的全局内存和堆内存、栈以及文件描述符。

1.1.1 POSIX线程接口

POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。

Pthreads定义了一套C语言的类型、函数与常量,它以pthread.h头文件和一个线程库实现。Pthreads API中大致共有100个函数调用,全都以pthread_开头,并可以分为四类:

  • 线程管理,例如创建线程,等待(join)线程,查询线程状态等。
  • 互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作
  • 条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作
  • 使用了互斥锁的线程间的同步管理

因此在编译时需要makefile的编译和链接选项:

CFLAGS+=-pthread # 编译选项
LDFLAGS+=-pthread # 链接选项
1.1.2 进程和线程

1.2 线程标识

就像每个进程有一个进程ID一样,每个线程也有一个线程ID。进程ID在整个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。

进程ID是用pid_t数据类型来表示的,是一个非负整数。线程ID是用pthread_t 数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。

因此需要一个函数来对两个线程ID进行比较:

#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

返回值:
    相等返回非0,否则返回0

获取自身的线程id:

#include <pthread.h>

pthread_t pthread_self(void);

返回值:
    调用线程的线程ID

8.3 线程创建
在传统 UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程(pthread)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。

新增的线程可以通过调用pthread_create函数创建。

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
                    void *(*start_routine) (void *), void *arg);

返回值:
    成功返回0,失败返回errno
  • thread:事先创建好的pthread_t类型的参数。成功时thread指向的内存单元被设置为新创建线程的线程ID。
  • attr:用于定制各种不同的线程属性。APUE的12.3节讨论了线程属性。通常直接设为NULL。
  • start_routine:新创建线程从此函数开始运行,无参数时arg设为NULL即可。形参是函数指针(该函数返回值和形参均为void*),因此需要传入函数地址。
  • arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。

代码示例

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

//线程执行的函数
static void *pthread_fn(void *s) 
{   
    printf("pthread_fn is working!\n");
    
    return NULL;
}

int main()
{
    pthread_t tid = 0;
    int err = 0;

    puts("Begin!");

    err = pthread_create(&tid, NULL, pthread_fn, NULL);
    if(err)
    {   
        printf("pthread_create is err!\n");
        return -1; 
    }   
    
    puts("end!");

    return 0;
}

执行结果: 

lei@ubuntu:~/Desktop/pthread$ ./a.out 
Begin!
end!

分析:线程的调度取决于调度器策略。创建线程后,新的线程还没来得及被调度,main线程就执行了return或exit(0)使得进程退出,所以新的线程并没有执行就退出了。

1.4 线程终止

1.4.1 终止方式

如果进程中的任意线程调用了 exit_Exit 或者 _exit,那么整个进程就会终止。与此相类似,如果默认的动作是终止进程,那么,发送到线程的信号就会终止整个进程。

单个线程可以通过3种方式退出,因此可以在不终止整个进程的情况下,停止它的控制流:

  • 线程可以简单地从启动例程中返回,返回值是线程的退出码
  • 线程可以被同一进程中的其他线程取消
  • 线程调用pthread_exit
#include <pthread.h>

void pthread_exit(void *rval_ptr);

参数:
    rval_ptr: 是一个无类型指针,
        进程中其他线程可以通过调用pthread_join函数访问到这个指针。

eg: 

//线程执行的函数
static void *pthread_fn(void *s) 
{   
    printf("pthread_fn is working!\n");
    
    pthread_exit(NULL);
    //return NULL;
}

函数pthread_join用来等待一个线程的结束。相当于进程控制中的wait。(调用线程将会一直阻塞,直至指定的线程调用上述三种退出方式)

#include <pthread.h>

int pthread_join(pthread_t thread, void **rval_ptr);

返回值:
    成功返回0
    失败返回错误编号
  • thread:为被等待的线程标识符
  • rval_ptr:为用户定义的指针,它可以用来存储被等待线程的返回值,即pthread_exit的参数。这是一个二级指针,因此传入的参数为一级指针的地址,如果不关心返回值则用NULL

代码示例1

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

//线程执行的函数
static void *pthread_fn(void *s)
{   
    printf("pthread_fn is working!\n");
    
    pthread_exit(NULL);
    //return NULL;
}

int main()
{
    pthread_t tid = 0;
    int err = 0;

    puts("Begin!");

    err = pthread_create(&tid, NULL, pthread_fn, NULL);
    if(err)
    {
        printf("pthread_create is err!\n");
        return -1;
    }
        
    pthread_join(tid, NULL);

    puts("end!");

    return 0;
}

执行结果: 

lei@ubuntu:~/Desktop/pthread$ ./a.out 
Begin!
pthread_fn is working!
end!

代码示例2

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

//线程执行的函数
static void *pthread_fn1(void *s)
{   
    printf("pthread_fn1 is working!\n");
    
    pthread_exit((void *)1);
    //return NULL;
}

static void *pthread_fn2(void *s)
{   
    printf("pthread_fn2 is working!\n");
    
    pthread_exit((void *)2);
}

int main()
{
    pthread_t tid1 = 0;
    pthread_t tid2 = 0;
    int err = 0;
    void *ptret = NULL;

    puts("Begin!");

    err = pthread_create(&tid1, NULL, pthread_fn1, NULL);
    if(err)
    {
        printf("pthread1_create is err!\n");
        return -1;
    }
    
    err = pthread_create(&tid2, NULL, pthread_fn2, NULL);
    if(err)
    {
        printf("pthread2_create is err!\n");
        return -1;
    }
        
    err = pthread_join(tid1, &ptret);
    if(err)
    {
        printf("pthread1_join is err!\n");
        return -1;
    }
    printf("thread1 exit code is %ld\n", (long)ptret);

    err = pthread_join(tid2, &ptret);
    if(err)
    {
        printf("pthread1_join is err!\n");
        return -1;
    }
    printf("thread2 exit code is %ld\n", (long)ptret);
    
    puts("end!");

    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/pthread$ ./a.out 
Begin!
pthread_fn1 is working!
thread1 exit code is 1
pthread_fn2 is working!
thread2 exit code is 2
end!
lei@ubuntu:~/Desktop/pthread$ ./a.out 
Begin!
pthread_fn1 is working!
pthread_fn2 is working!
thread1 exit code is 1
thread2 exit code is 2
end!
1.4.2 栈的清理

线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数(钩子函数)安排退出是类似的。这样的函数称为线程清理处理程序(thread cleanup handler)。一个线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说,它们的执行顺序与它们注册时相反。

#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

当线程执行以下动作时,清理函数routine是由pthread_cleanup_push函数调度的,调用时只有一个参数arg:

  • 调用pthread_exit时;
  • 响应取消请求时;
  • 用非零execute参数调用pthread_cleanup_pop 时。

如果 execute 参数设置为0,清理函数将不被调用。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次 pthread_cleanup_push调用建立的清理处理程序。

注意:这些函数有一个限制,由于它们可以实现为,所以必须在与线程相同的作用域中以匹配对的形式使用。pthread_cleanup_push 的宏定义可以包含字符{,这种情况下,在 pthread cleanup_pop 的定义中要有对应的匹配字符}

代码示例

include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void *cleanup_fun(void *s)
{
    puts(s);
}

static void *pthread_fun(void *s)
{
    puts("thread is working!");

    pthread_cleanup_push(cleanup_fun, "cleanup 1");
    pthread_cleanup_push(cleanup_fun, "cleanup 2");
    pthread_cleanup_push(cleanup_fun, "cleanup 3");
    
    puts("push over!");
//成对出现
    pthread_cleanup_pop(1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(1);

    pthread_exit((void *)1);
}

int main()
{
    pthread_t tid = 0;
    int err = 0;
    void *pret;

    puts("begin!");

    err = pthread_create(&tid, NULL, pthread_fun, NULL);
    if (err)
    {
        fprintf(stderr, "pthread_create: %s\n", strerror(err));
        exit(1);
    }

    pthread_join(tid, &pret);
    printf("pthread exit code is %ld\n", (long)pret);
    puts("end!");

    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/pthread$ ./a.out 
begin!
thread is working!
push over!
cleanup 3
cleanup 1
pthread exit code is 1
end!

1.4.3 线程的取消
多线程程序中,一个线程可以借助 pthread_cancel() 函数向另一个线程发送“终止执行”的信号,从而令目标线程结束执行。

pthread_cancel调用并不等待线程终止,它只提出请求。线程在取消请求发出后会继续运行,直到到达某个取消点(CancellationPoint)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。

与线程取消相关的函数有:

int pthread_cancel(pthread_t thread);

功能:
    发送终止信号给thread线程
返回值:
    如果成功则返回0,否则为非0值。
        发送成功并不意味着thread会终止。
int pthread_setcanceltype(int type, int *oldtype)  

功能:
    设置本线程取消动作的执行时机
返回值:
    成功返回0
    失败返回错误编号
void pthread_testcancel(void)

功能:
    在不包含取消点,但是又需要取消点的地方创建一个取消点,
   以便在一个没有包含取消点的执行代码线程中响应取消请求

1.5 线程同步

1.5.1 概念和例子

五个基本的同步机制:

        互斥量,读写锁,条件变量,自旋锁,屏障

互斥量要避免死锁,带有超时的互斥量

带有超时的读写锁

线程竞争的实例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM (RIGHT - LEFT + 1)

static void *thread_fun(void *s)
{
    int i, j, mark;
    i = *(int *)s;
    mark = 1;
    for(j = 2; j < i/2; j++) {
        if(i % j == 0) {
            mark = 0;
            break;
        }
    }
    if(mark)
        printf("%d is a primer.\n", i);

    pthread_exit(NULL);
}

int main()
{
    int i, err;
    pthread_t tid[THREADNUM];

    puts("begin!");

    for(i = LEFT; i <= RIGHT; i++)
    {
        err = pthread_create(tid+(i-LEFT), NULL, thread_fun, &i);
        if (err)
        {
            fprintf(stderr, "pthread_create: %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = LEFT; i <= RIGHT; i++)
    {
        pthread_join(tid[i-LEFT], NULL);
    }
        
    puts("end!");

    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/pthread$ ./a.out 
begin!
3000017 is a primer.
3000077 is a primer.
3000047 is a primer.
3000181 is a primer.
3000181 is a primer.
end!
lei@ubuntu:~/Desktop/pthread$ ./a.out 
begin!
3000017 is a primer.
3000017 is a primer.
3000199 is a primer.
3000181 is a primer.
end!

结果每次都不一样。

原因:线程发生了竞争。

创建线程时,main线程传递给函数thread_fun的参数&i是同一个地址,但是地址保存的值不相同。

解决竞争

定义一个结构体,成员为要计算判断的数,然后每次动态分配内存,将地址作为线程函数的参数即可。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM (RIGHT - LEFT + 1)

struct thr_arg_st{
    int n;
};

static void *thread_fun(void *s)
{
    int i, j, mark;
    //先将void*强转为struct thr_arg_st *
    i = ((struct thr_arg_st *)s) -> n;
    mark = 1;

    for(j = 2; j < i/2; j++) {
        if(i % j == 0) {
            mark = 0;
            break;
        }
    }
    if(mark)
        printf("%d is a primer.\n", i);

    pthread_exit(s);//将s作为返回值
}

int main()
{
    int i, err;
    pthread_t tid[THREADNUM];
    struct thr_arg_st *p;
    void *ptr;

    puts("begin!");

    for(i = LEFT; i <= RIGHT; i++)
    {
        p = malloc(sizeof(*p));
        if (p == NULL)
        {
            perror("malloc()");
            return -1;
        }
        p->n = i;

        err = pthread_create(tid+(i-LEFT), NULL, thread_fun, p);
        if (err)
        {
            fprintf(stderr, "pthread_create: %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = LEFT; i <= RIGHT; i++)
    {
        pthread_join(tid[i-LEFT], &ptr);
        free(ptr);//释放动态分配的内存
    }
        
    puts("end!");

    return 0;
}

当多个控制线程共享相同的内存时,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题。同样,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是,当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。

当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。当然,这种行为是与处理器体系结构相关的,但是可移植的程序并不能对使用何种处理器体系结构做出任何假设。

图11-7描述了两个线程读写相同变量的假设例子。在这个例子中,线程A读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。当线程B在这两个存储器写周期中间读取这个变量时,它就会得到不一致的值。

为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。图11-8描述了这种同步。如果线程B希望读取变量,它首先要获取锁。同样,当线程A更新变量时,也需要获取同样的这把锁。这样,线程B在线程A释放锁以前就不能读取变量。

1.5.2 互斥量

可以使用 pthread 的互斥接口来保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。

对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。

只有将所有线程都设计成遵守相同数据访问规则的,互斥机制才能正常工作。操作系统并不会为我们做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。

互斥变量是用 pthread_mutex_t 数据类型表示的。在使用互斥变量以前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER(只适用于静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如,通过调用malloc函数),在释放内存前需要调用pthread_mutex_destroy

相关函数:

  • 初始化和销毁:
#include <pthread.h>
// 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                        const pthread_mutexattr_t *restrict attr);

// 静态分配互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

返回值:
    成功返回0
    失败返回错误编号
  • 加锁和解锁
#include <pthread.h>

// 阻塞加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 非阻塞加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:
    成功返回0
    失败返回错误编号
        pthread_mutex_trylock锁住互斥量,返回0.
                             不能锁住互斥量,返回EBUSY

代码示例——20个线程读写一个文件

先向/tmp/out下入1,然后创建20个线程来读这个文件内容并加1,然后写入。期望内容为21

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

#define FNAME "/tmp/out"
#define THRNUM 20
#define LINESIZE 1024

static void *thr_add(void *p) 
{
    FILE *fp;
    char linebuf[LINESIZE];

    fp = fopen(FNAME, "r+");
    if(fp == NULL) 
    {
        perror("fopen()");
        exit(1);
    }

    fgets(linebuf, LINESIZE, fp);
    // 读完后将文件指针指向文件起始处
    fseek(fp, 0, SEEK_SET);
    sleep(1);
    // 向文件写入内容
    fprintf(fp, "%d", atoi(linebuf) + 1);
    fclose(fp);

    pthread_exit(NULL);
}

int main(void) 
{
    pthread_t tid[THRNUM];
    int i, err;

    for(i = 0; i < THRNUM; i++) 
    {
        err = pthread_create(tid + i, NULL, thr_add, NULL);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = 0; i < THRNUM; i++)
        pthread_join(tid[i], NULL);

    //读取文件中数据
    FILE *fp;

    fp = fopen(FNAME, "r");
    if(fp == NULL)
    {
        perror("fopen()");
        exit(1);
    }
    
    char buf[1024];
    
    fseek(fp, 0, SEEK_SET);
    fgets(buf, LINESIZE, fp);
    printf("buf = %s\n", buf);

    fclose(fp);

    exit(0);
}

 执行结果:

lei@ubuntu:~/Desktop/pthread$ echo 1 > /tmp/out
lei@ubuntu:~/Desktop/pthread$ ./a.out 
buf = 2
lei@ubuntu:~/Desktop/pthread$ ./a.out 
buf = 3
lei@ubuntu:~/Desktop/pthread$ ./a.out 
buf = 4

 分析:由于调度和竞争,线程读到文件内容1后,休眠1s,然后一起写入2,所以结果就为2。

补充

#include <stdlib.h>
int atoi(const char *str)

作用:
    C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。
参数:
    str -- 要转换为整数的字符串。
返回值:
    该函数返回转换后的长整数,
    如果没有执行有效的转换,则返回零。

代码示例——互斥量的使用

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

#define FNAME "/tmp/out"
#define THRNUM 20
#define LINESIZE 1024

//初始化互斥量,设置为常量PTHREAD_MUTEX_INITIALIZER,也可以使用init初始化
//PTHREAD_MUTEX_INITIALIZER只适用于静态分配的互斥量
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;

static void *thr_add(void *p) 
{
    FILE *fp;
    char linebuf[LINESIZE];

    fp = fopen(FNAME, "r+");
    if(fp == NULL) 
    {
        perror("fopen()");
        exit(1);
    }
    
    //进入临界区加上互斥锁
    pthread_mutex_lock(&mut);

    fgets(linebuf, LINESIZE, fp);
    // 读完后将文件指针指向文件起始处
    fseek(fp, 0, SEEK_SET);
    sleep(1);
    // 向文件写入内容
    fprintf(fp, "%d", atoi(linebuf) + 1);
    fclose(fp);

    //退出临界区后解锁
    pthread_mutex_unlock(&mut);

    pthread_exit(NULL);
}

int main(void) 
{
    pthread_t tid[THRNUM];
    int i, err;

    for(i = 0; i < THRNUM; i++) 
    {
        err = pthread_create(tid + i, NULL, thr_add, NULL);
        if(err) {
            fprintf(stderr, "pthread_create(): %s\n", strerror(err));
            exit(1);
        }
    }

    for(i = 0; i < THRNUM; i++)
        pthread_join(tid[i], NULL);

    //读取文件中数据
    FILE *fp;

    fp = fopen(FNAME, "r");
    if(fp == NULL)
    {
        perror("fopen()");
        exit(1);
    }
    
    char buf[1024];
    
    fseek(fp, 0, SEEK_SET);
    fgets(buf, LINESIZE, fp);
    printf("buf = %s\n", buf);

    fclose(fp);

    //销毁互斥锁
    pthread_mutex_destroy(&mut);

    exit(0);
}

执行结果:

lei@ubuntu:~/Desktop/pthread$ echo 1 > /tmp/out
lei@ubuntu:~/Desktop/pthread$ cat /tmp/out 
1
lei@ubuntu:~/Desktop/pthread$ ./a.out 
buf = 21

代码示例3——使用互斥锁实现线程同步

需求:四个线程依次打印abcd

锁链式同步

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

#define PTHNUM 4
pthread_mutex_t mut[PTHNUM];

// 返回下一个线程的编号0~3,3的下一个为0
static int next(int n)
{
    return (n + 1) == PTHNUM ? 0 : n + 1;
}

static void *pth_fun(void *p)
{
    int c = 'a' + (int)p;//线程打印的字符
    int n = (int)p;//线程编号

    while(1)
    {
        //加锁
        pthread_mutex_lock(mut + n);
        write(1, &c, 1);
        //释放下一个线程的锁
        pthread_mutex_unlock(mut + next(n));
    }

    pthread_exit(NULL);
}

int main()
{
    pthread_t tid[PTHNUM];
    int i = 0, err;
    
    for (i = 0; i < PTHNUM; i++)
    {
        //初始化互斥锁
        pthread_mutex_init(mut + i, NULL);
        //加锁
        pthread_mutex_lock(mut + i);
        err = pthread_create(tid + i, NULL, pth_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_create:%s\n", strerror(err));
            exit(1);
        }
    }
    
    //释放打印a线程的锁
    pthread_mutex_unlock(mut + 0);

    alarm(5);

    for (i = 0; i < PTHNUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    return 0;
}
1.5.3 线程池

线程数是有一定限制的,1.5.1节用201个线程来检测质数,本节利用线程池来解决。

使用互斥锁让四个线程争抢方式处理数据。

假设线程池提供4个线程来检测201个质数。设置临界区资源num,当:

  • num = 0:当前没有任务
  • num = -1:当前任务已经全部完成
  • num = 300000~3000200:当前有一个任务,需要一个线程来接受任务
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM 4 //线程池中有四个线程

static int num; // 临界区资源
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER; // 静态互斥量定义

static void *thread_fun(void *s)
{
    int i, j, mark;
    //领取任务直至结束
    while(1)
    {   
        pthread_mutex_lock(&mut_num);
        //循环检测是否有任务
        while(num == 0)//没有任务
        {
            pthread_mutex_unlock(&mut_num);
            sched_yield(); // 让出CPU
            pthread_mutex_lock(&mut_num);
        }

        //拿到任务,判断是否为-1,是则结束,不是则处理任务后继续等待
        if(num == -1)
        {
            //释放锁后再退出,防止死锁
            pthread_mutex_unlock(&mut_num);
            break;
        }
        //拿到任务
        i = num;
        //将任务置为0
        num = 0;
        pthread_mutex_unlock(&mut_num);
        //线程做任务
        mark = 1;
        for(j = 2; j < i/2; j++) {
            if(i % j == 0) {
                mark = 0;
                break;
            }
        }
        if(mark)
            printf("[%d]%d is a primer.\n", (int)s, i);

    }
    
    
    pthread_exit(NULL);
}

int main()
{
    int i, err;
    pthread_t tid[THREADNUM];

    puts("begin!");
    
    //创建四个线程
    for(i = 0; i < THREADNUM; i++)
    {
        err = pthread_create(tid+i, NULL, thread_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_create: %s\n", strerror(err));
            exit(1);
        }
    }

    //主线程下发任务
    for(i = LEFT; i <= RIGHT; i++)
    {
        pthread_mutex_lock(&mut_num);
        //循环检测任务是否被领走
        while(num != 0)
        {
            pthread_mutex_unlock(&mut_num);
            sched_yield(); // 让出CPU
            pthread_mutex_lock(&mut_num);
        }
        //设置num,即下发任务
        num = i;
        pthread_mutex_unlock(&mut_num);
    }//任务下发完毕

//设置num == -1 ,代表任务全部结束
    pthread_mutex_lock(&mut_num);
    //循环检测最后一个任务是否被领走
    while(num != 0)
    {
        pthread_mutex_unlock(&mut_num);
        sched_yield();
        pthread_mutex_lock(&mut_num);
    }
    num = -1;
    pthread_mutex_unlock(&mut_num);

    //收尸
    for(i = 0; i < THREADNUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    //销毁互斥锁
    pthread_mutex_destroy(&mut_num);
        
    puts("end!");

    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/pthread$ ./a.out 
begin!
[0]3000029 is a primer.
[1]3000017 is a primer.
[0]3000073 is a primer.
[3]3000047 is a primer.
[2]3000061 is a primer.
[0]3000089 is a primer.
[3]3000103 is a primer.
[0]3000133 is a primer.
[2]3000131 is a primer.
[1]3000077 is a primer.
[0]3000161 is a primer.
[2]3000199 is a primer.
[3]3000181 is a primer.
end!

不足:该程序存在盲等,即查询法的不足,上游main线程一直在循环查看任务是否被领走,而下游一直在循环查看是否有任务。

通知法:上游将设置任务后,唤醒下游来处理任务。如果没有领走任务,则阻塞自己,等待下游来唤醒。

下游发现有任务,则领走任务,并唤醒上游;没有任务,则阻塞,等待上游来唤醒。

1.5.4 线程令牌桶
1.5.5 条件变量

条件变量是线程可用的另一种同步机制。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件。

在使用条件变量之前,必须先对它进行初始化。由pthread_cond_t数据类型表示的条件变量可以用两种方式进行初始化。可以把常量PTHREAD_COND_INITTALIZER赋给静态分配的条件变量但是如果条件变量是动态分配的,则需要使用pthread_cond_init函数对它进行初始化。在释放条件变量底层的内存空间之前,可以使用pthread_cond_destroy函数对条件变量进行反初始化。

相关函数和作用:

初始化条件变量

#include <pthread.h>
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);

// 动态初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

// 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

返回值:
    成功返回0
    失败返回错误编号

阻塞当前线程,等待条件的成立

#include <pthread.h>

// 有时间的条件等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                           pthread_mutex_t *restrict mutex,
                           const struct timespec *restrict abstime);

// 条件等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
                      pthread_mutex_t *restrict mutex);

返回值:
    成功返回0
    失败返回错误编号
  • cond:已初始化好的条件变量
  • mutex:与条件变量配合使用的互斥锁
  • abstime:阻塞线程的时间

调用两个函数之前,我们必须先创建好一个互斥锁并完成加锁操作,然后才能作为实参传递给 mutex 参数。两个函数会完成以下两项工作:

  • 阻塞线程,直至接收到条件成立的信号
  • 当线程被添加到等待队列上时,将互斥锁解锁,即释放mutex

也就是说,函数尚未接收到“条件成立”的信号之前,它将一直阻塞线程执行。注意,当函数接收到“条件成立”的信号后,它并不会立即结束对线程的阻塞,而是先完成对互斥锁的“加锁”操作,然后才解除阻塞。

两个函数都以“原子操作”的方式完成“阻塞线程+解锁”或者“重新加锁+解除阻塞”这两个过程。

解除线程的“阻塞”状态(唤醒)

#include <pthread.h>
// 唤醒所有的阻塞线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 唤醒所有正在的至少一个线程
int pthread_cond_signal(pthread_cond_t *cond);

返回值:
    成功返回0
    失败返回错误编号

对于被上面两个函数阻塞的线程,我们可以借助如上两个函数向它们发送“条件成立”的信号,解除它们的“阻塞”状态。

由于互斥锁的存在,解除阻塞后的线程也不一定能立即执行。当互斥锁处于“加锁”状态时,解除阻塞状态的所有线程会组成等待互斥锁资源的队列,等待互斥锁“解锁”。

代码示例1——查询法转通知法

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define LEFT 3000000
#define RIGHT 3000200
#define THREADNUM 4 //线程池中有四个线程

static int num; // 临界区资源
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER; // 静态互斥量定义
static pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER; //静态条件变量的定义

static void *thread_fun(void *s)
{
    int i, j, mark;
    //领取任务直至结束
    while(1)
    {   
        pthread_mutex_lock(&mut_num);
        //循环检测是否有任务
        while(num == 0)//没有任务
        {
            //阻塞自己,并释放互斥锁
            pthread_cond_wait(&cond_num, &mut_num);
        }

        //拿到任务,判断是否为-1,是则结束,不是则处理任务后继续等待
        if(num == -1)
        {
            //释放锁后再退出,防止死锁
            pthread_mutex_unlock(&mut_num);
            break;
        }
        //拿到任务
        i = num;
        //将任务置为0
        num = 0;
        //唤醒等待该条件的所有线程
        pthread_cond_broadcast(&cond_num);
        pthread_mutex_unlock(&mut_num);
        //线程做任务
        mark = 1;
        for(j = 2; j < i/2; j++) {
            if(i % j == 0) {
                mark = 0;
                break;
            }
        }
        if(mark)
            printf("[%d]%d is a primer.\n", (int)s, i);

    }
    
    
    pthread_exit(NULL);
}

int main()
{
    int i, err;
    pthread_t tid[THREADNUM];

    puts("begin!");
    
    //创建四个线程
    for(i = 0; i < THREADNUM; i++)
    {
        err = pthread_create(tid+i, NULL, thread_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_create: %s\n", strerror(err));
            exit(1);
        }
    }

    //主线程下发任务
    for(i = LEFT; i <= RIGHT; i++)
    {
        pthread_mutex_lock(&mut_num);
        //循环检测任务是否被领走
        while(num != 0)
        {
            //阻塞自己,并释放互斥锁
            pthread_cond_wait(&cond_num, &mut_num);
        }
        //设置num,即下发任务
        num = i;

        //唤醒等待该条件的所有线程
        pthread_cond_broadcast(&cond_num);
        pthread_mutex_unlock(&mut_num);
    }//任务下发完毕

//设置num == -1 ,代表任务全部结束
    pthread_mutex_lock(&mut_num);
    //循环检测最后一个任务是否被领走
    while(num != 0)
    {
       //阻塞自己,并释放互斥锁
        pthread_cond_wait(&cond_num, &mut_num);
    }
    num = -1;
    //唤醒等待该条件的所有线程
    pthread_cond_broadcast(&cond_num);
    pthread_mutex_unlock(&mut_num);

    //收尸
    for(i = 0; i < THREADNUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    //销毁互斥量,条件变量
    pthread_mutex_destroy(&mut_num);
    pthread_cond_destroy(&cond_num);   

    puts("end!");

    return 0;
}

代码示例2——打印abcd

条件变量+互斥锁 = 通知法同步

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

#define PTHNUM 4

static int num = 0;
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 返回下一个线程的编号0~3,3的下一个为0
static int next(int n)
{
    return (n + 1) == PTHNUM ? 0 : n + 1;
}

static void *pth_fun(void *p)
{
    int c = 'a' + (int)p;//线程打印的字符
    int n = (int)p;//线程编号

    while(1)
    {
        //加锁
        pthread_mutex_lock(&mut);

        while(num != n)
        {
            //判断是不是自己编号,是继续,不是则阻塞并释放锁
            pthread_cond_wait(&cond, &mut);
        }
        write(1, &c, 1);
        num = next(num);

        //唤醒等待该条件的所有线程
        pthread_cond_broadcast(&cond);
        pthread_mutex_unlock(&mut);
    }

    pthread_exit(NULL);
}

int main()
{
    pthread_t tid[PTHNUM];
    int i = 0, err;
    
    for (i = 0; i < PTHNUM; i++)
    {
        err = pthread_create(tid + i, NULL, pth_fun, (void *)i);
        if (err)
        {
            fprintf(stderr, "pthread_create:%s\n", strerror(err));
            exit(1);
        }
    }

    alarm(5);

    for (i = 0; i < PTHNUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    pthread_mutex_destroy(&mut);
    pthread_cond_destroy(&cond);

    return 0;
}
1.5.6 信号量

使用互斥量和条件变量可以实现信号量的功能。

1.6 线程属性

pthread 接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。通常,管理这些属性的函数都遵循相同的模式。

在所有调用pthread_create函数的实例中,传入的参数都是空指针,而不是指向pthread_attr_t结构的指针。可以使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。

可以使用pthread_attr_init函数初始化 pthread_attr_t 结构。在调用 pthread attr_init 以后,pthread_attr_t 结构所包含的就是操作系统实现支持的所有线程属性的默认值。

初始化和销毁

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

返回值:
    成功返回0
    失败返回错误编号

下图总结了 POSIX.1 定义的线程属性。POSIX.1 还为线程执行调度(Thread Execution Scheduling)选项定义了额外的属性,用以支持实时应用。下图同时给出了各个操作系统平台对每个线程属性的支持情况。

线程分离状态属性

线程分离:在我们使用默认属性创建一个线程的时候,线程是 joinable 的。 joinable 状态的线程,必须在另一个线程中使用 pthread_join() 等待其结束, 如果一个 joinable 的线程在结束后,没有使用 pthread_join() 进行操作, 这个线程就会变成”僵尸线程”。可以使用pthread_detach函数让线程分离。

当线程被设置为分离状态后,线程结束时,它的资源会被操作系统自动的回收, 而不再需要在其它线程中对其进行 pthread_join() 操作。

#include <pthread.h>
// 设置状态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 获取状态
int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

返回值:
    成功返回0
    失败返回错误编号

detachstate:可以设置为以下属性

  • PTHREAD_CREATE_DETACHED:线程分离状态
  • PTHREAD_CREATE_JONINABLE:线程可joinable状态

线程的栈和栈大小

可以使用下列函数设置线程的栈属性。

#include <pthread.h>

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

返回值:
    成功返回0
    失败返回错误编号

对于进程来说,虚地址空间的大小是固定的。因为进程中只有一个栈,所以它的大小通常不是问题。但对于线程来说,同样大小的虚地址空间必须被所有的线程栈共享。如果应用程序使用了许多线程,以致这些线程栈的累计大小超过了可用的虚地址空间,就需要减少默认的线程栈大小。另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(stack frame),那么需要的栈大小可能要比默认的大。

如果线程栈的虚地址空间都用完了,那可以使用malloc或者mmap来为可替代的栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。由stackaddr 参数指定的地址可以用作线程视的内容范围中的最低可寻找地址,该地址与处理器结构相应的边界应对齐。当然,这要假设malloc和mmap所用的虚地址范围与线程栈当前使用的虚地址范围不同。

stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈的开始位置。对于一个给定的处理器结构来说,如果栈是从高地址向低地址方向增长的,那么 stackaddr线程属性将是栈的结尾位置,而不是开始位置。

应用程序也可以通过下列函数读取或设置线程属性stacksize

#include <pthread.h>

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);

返回值:
    成功返回0
    失败返回错误编号

如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性时,选择的stacksize不能小于PTHREAD_STACK_MIN

代码示例——测试线程数量的上限:


 

1.7 同步属性

线程的同步对象也具有属性。

1.7.1 互斥量属性

互斥量属性是用 pthread_mutexattr_t 结构表示的。在1.5.2节每次对互斥量进行初始化时,都是通过使用PTHREAD_MUTEX_INITTALIZER 常量或者用指向互斥量属性结构的空指针作为参数调用 pthread_mutex_init 函数,得到互斥量的默认属性。

对于非默认属性,可以使用下列函数进行初始化和销毁:

#include <pthread.h>

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

返回值:
    成功返回0
    失败返回错误编号

互斥量的三个主要属性:

  • 进程共享属性
  • 健壮属性(略)
  • 类型属性

进程共享

在进程中,多个线程可以访问同一个同步对象。正如在1.5.2节看到的,这是默认的行为。在这种情况下,进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE

但也存在这样的机制:允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。

相关函数调用:

#include <pthread.h>

//获得进程共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared);
//修改进程共享属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

返回值:
    成功返回0
    失败返回错误编号

pshared:这里的p指的就是进程process,进程共享

  • PTHREAD_PROCESS_PRIVATE:进程独占互斥量(只有初始化的那个进程内的多个线程可用)
  • PTHREAD_PROCESS_SHARED:进程共享互斥量(多进程中的多个线程可用)

类型属性

类型互斥量属性控制着互斥量的锁定特性。POSIX.1定义了4种类型。

  • PTHREAD_MUTEX_NORMAL:一种标准互斥量类型,不做任何特殊的错误检查或死锁检测。
  • PTHREAD_MUTEX_ERRORCHECK:此互斥量类型提供错误检查。
  • PTHREAD_MUTEX_RECURSIVE :此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁。所以,如果对一个递归互斥量加锁两次,然后解锁一次,那么这个互斥量将依然处于加锁状态,对它再次解锁以前不能释放该锁。
  • PTHREAD_MUTEX_DEFAULT:此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。


上图不占用解锁是指解锁不是自己加的锁(解锁别人加的锁),例如打印abcd的程序(互斥量+条件变量)。

相关函数:

#include <pthread.h>

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

返回值:
    成功返回0
    失败返回错误编号
1.7.2 读写锁属性 

唯一个属性称为进程共享属性 

1.7.3 条件变量属性

Single UNIX Specification目前定义了条件变量的两个属性:进程共享属性和时钟属性。 

相关函数:

#include <pthread.h>

int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);

int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared);

返回值:
    成功返回0
    失败返回错误编号
1.7.4 自旋锁属性

 一个属性称为进程共享属性 

1.7.5 屏障属性

目前定义的属性只有进程共享属性 

1.8 线程安全IO

1.9 线程和信号

1.10 线程与fork

5 高级IO

本节对应第十四章高级IO

IO模型分为五种:

  • 阻塞io
  • 非阻塞io
  • 信号驱动
  • 多路转接
  • 异步io

1.1 BIO和NIO(阻塞和不阻塞)

1.2 终端IO

1.3 有限状态机 

1.4 IO多路转接模型

IO多路转接也称为IO多路复用。为了大量的描述符IO事件就绪监控。

程序均不会出现盲等状态。

IO复用模型核心思路:系统给我们提供一类函数(select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。

1.4.1 select

函数原型:

/* According to POSIX.1-2001 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

返回值:
    错误返回-1
    超时返回0
    成功返回发生事件的文件描述符个数。

// 删除 set 中的给定的文件描述符
void FD_CLR(int fd, fd_set *set);
// 测试文件描述符 fd 是否在 set 集合中
int  FD_ISSET(int fd, fd_set *set);
    返回值:成功返回非0,失败返回0
// 将文件描述符 fd 添加到 set 中
void FD_SET(int fd, fd_set *set);
// 清空 set 中的文件描述符
void FD_ZERO(fd_set *set);

select参数含义:

  • nfds:最大的文件描述符 + 1;
  • readfds:需要监视的输入文件描述符集合,底层采用数组存储。
  • writefds:需要监视的输出文件描述符集合;
  • exceptfds:需要监视的会发生异常的文件描述符集合;
  • timeout:等待的超时时间,如果时间超时依然没有文件描述符状态发生变化那么就返回。设置为 0 会立即返回,设置为 NULL 则一直阻塞等待,不会超时

监听流程:

select操作流程

代码示例1----对标准输入进行监听

#include <stdio.h>
#include <stdlib.h>
#include <sys/select.h>

int main(int argc, char const *argv[])
{
    fd_set rfds;

    while(1)
    {
        // void FD_ZERO(fd_set *set);
        FD_ZERO(&rfds);//初始化集合
        FD_SET(0, &rfds);//将标准输入添加到集合中
        /*
            int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
        */
        int maxfd = 0;
        struct timeval tv;
        tv.tv_sec = 3;
        tv.tv_usec = 0;

        int nfs = select(maxfd + 1, &rfds, NULL, NULL, &tv);
        if (nfs < 0)
        {
            perror("select error");
            return -1;
        }
        else if (nfs == 0)
        {
            printf("select timeout!\n");
            continue;
        }

        for(int i = 0; i <= maxfd; i++)
        {
            if (FD_ISSET(i, &rfds) != 0)
            {   //就绪了可读事件
                char buf[1024] = {0};
                read(0, buf, 1024);
                printf("buf:%s", buf);
            }
        }
    }

    return 0;
}

 执行结果:

lei@ubuntu:~/Desktop/muti-io$ ./select 
select timeout!
select timeout!
hello
buf:hello
select timeout!

select的缺点:

  • 监听的IO最大连接数有限,取决于_FD_SETSIZE,默认1024,在Linux系统上一般为1024。
  • select函数返回后,是通过(轮询)遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流) ,如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。
  • 内存拷贝:需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
1.4.2 poll

select 和 poll 系统调用的本质一样,poll 的机制与 select 类似,与 select 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll 没有最大文件描述符数量的限制(数量过大后性能也是会下降)。poll 和 select 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

函数原型:

// poll - wait for some event on a file descriptor

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

返回值:
    成功返回准备就绪的描述符数目
    失败返回-1
    超时返回0

struct pollfd {
    int   fd;         /* 需要监视的文件描述符 */
    short events;     /* 要监视的事件 */
    short revents;    /* 该文件描述符发生了的事件 */
};

参数含义:

  • fds:实际上是一个结构体数组的首地址,因为 poll 可以帮助我们监视多个文件描述符,而一个文件描述放到一个 struct pollfd 结构体中,多个文件描述符就需要一个数组来存储了(一个文件描述符对应一个结构体)。底层采用链表存储。
  • nfds:fds 这个数组的长度。
  • timeout:阻塞等待的超时时间。传入 -1 则始终阻塞,不超时。(毫秒)

结构体中的事件eventsrevents可以指定下面七种事件,同时监视多个事件可以使用按位或(|)添加:

事件描述
POLLIN文件描述符可读
POLLPRI可以非阻塞的读高优先级的数据
POLLOUT文件描述符可写
POLLRDHUP流式套接字连接点关闭,或者关闭写半连接。
POLLERR已出错
POLLHUP已挂断(一般指设备)
POLLNVAL参数非法

监控流程:

poll操作流程

#include <stdio.h>
#include <poll.h>

#define MAX_POLL_SIZE 10

int main(int argc, char const *argv[])
{
    //定义时间结构体数组
    struct pollfd fds[MAX_POLL_SIZE];
    //添加要监控的描述符时间信息
    fds[0].fd = 0;
    fds[0].events = POLLIN;

    int count = 1;

    while(1)
    {
        int ret = poll(fds, MAX_POLL_SIZE, 3000);//timeout 3000毫秒=3秒
        if (ret < 0)
        {
            perror("poll error");
            return -1;
        }
        else if(ret == 0)
        {
            printf("poll timeout!\n");
            continue;
        }

        for(int i = 0; i < ret; i++)
        {
            if (fds[i].revents & POLLIN)
            {
                char buf[1024] = {0};
                read(fds[i].fd, buf, 1024);
                printf("buf:%s", buf);
            }
        }
    }
    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/muti-io$ ./a.out 
poll timeout!
poll timeout!
hello
buf:hello
poll timeout!

poll的优点:

        1.使用事件结构,替代了事件集合,相较于select操作简便性提高了很多

        2.所能监控的描述符数量不再做上线限制

poll的缺点:

        1.每次监控需要将信息拷贝到内核中

        2.监控原理涉及到多次对事件数组的遍历,因此性能会随着描述符增加而降低

        3.每次监控完毕后,依然需要遍历整个事件数组,才能确定那个描述符就绪哪个事件

1.4.3 epoll

select和poll在需要我们在用户态创建监视文件描述符的集合(fd_setpollfd,底层分别采用数组和链表存储,因此前者有大小限制,后者没有),调用时,需要将该集合复制到内核空间中,这样内核才能帮助我们轮询fd,这个过程具有一定开销。

epoll则只提供这个集合创建、控制相关的接口,调用时,直接在内核空间创建监视fd的集合,因此去除了复制过程开销。过程如下:

相关调用:epoll_createepoll_ctlepoll_wait

epoll_create

#include <sys/epoll.h>
int epoll_create(int size);

参数:调用 epoll_create 时最初 size 参数给传入多少,内核在建立数组的时候就是多少个元素。后来改进为只要 size 传入一个正整数即可,内核不会再根据传入的 size 直接作为数组的长度,因为内核是使用 hash 来管理要监视的文件描述符的。

作用和返回值:该函数会创建一个 epoll 实例(或epoll对象),同时返回一个引用该实例的文件描述符返回的文件描述符仅仅指向对应的 epoll 实例,并不表示真实的磁盘文件节点。其他 API 如 epoll_ctl、epoll_wait 会使用这个文件描述符来操作相应的 epoll 实例,需要手动释放这个文件描述符。

一个epoll对象都有一个独立的eventpoll结构体,结构体如下:

struct eventpoll {
    ...
    /*红黑树的根节点,这颗树存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表存储所有就绪的文件描述符*/
    struct list_head rdlist;
    ...
};

epoll 实例内部存储:

  • 监听列表:所有要监听的文件描述符,使用红黑树,由 epoll_ctl 传来
  • 就绪列表:所有就绪的文件描述符,使用双向链表

epoll_ctl

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

struct epoll_event {
       uint32_t events; /* Epoll 监视的事件,这些事件与 poll 能监视的事件差不多,只是宏名前面加了个E */
       epoll_data_t data; /* 用户数据,除了能保存文件描述符以外,还能让你保存一些其它有关数据,比如你这个文件描述符是嵌在一棵树上的,你在使用它的时候不知道它是树的哪个节点,则可以在布置监视任务的时候将相关的位置都保存下来。这个联合体成员就是 epoll 设计的精髓。 */
};

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

参数和返回值:

  • epfd: 即 epoll_create 返回的文件描述符,指向一个 epoll 实例
  • fd: 表示要监听的目标文件描述符
  • event: 表示要监听的事件(可读、可写、发送错误…)
  • op: 表示要对 fd 执行的操作,有以下几种:

    • EPOLL_CTL_ADD:为 fd 添加一个监听事件 event
    • EPOLL_CTL_MOD:event 是一个结构体变量,这相当于变量 event 本身没变,但是更改了其内部字段的值
    • EPOLL_CTL_DEL:删除 fd 的所有监听事件,这种情况下 event 参数没用
  • 返回值 0 或 -1,表示上述操作成功与否。

作用:epoll_ctl 会将文件描述符 fd 添加到 epoll 实例的监听列表里,同时为 fd 设置一个回调函数,并监听事件 event,如果红黑树中已经存在立刻返回。当 fd 上发生相应事件时,会调用回调函数,将 fd 添加到 epoll 实例的就绪队列上。

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数和返回值:

  • epfd:要操作的 epoll 实例;
  • events: 是一个数组,保存就绪状态的文件描述符,其空间由调用者负责申请
  • maxevents: 指定 events 的大小
  • timeout: 类似于 select 中的 timeout。如果没有文件描述符就绪,即就绪队列为空,则 epoll_wait 会阻塞 timeout 毫秒。如果 timeout 设为 -1,则 epoll_wait 会一直阻塞,直到有文件描述符就绪;如果 timeout 设为 0,则 epoll_wait 会立即返回
  • 返回值: events 中存储的就绪描述符个数,最大不超过 maxevents。=0表示超时。<0表示出错。

三者区别

selectpollepoll
底层数据结构数组存储文件描述符链表存储文件描述符红黑树存储监控的文件描述符,双链表存储就绪的文件描述符
如何从fd数据中获取就绪的fd遍历fd_set遍历链表回调
时间复杂度获得就绪的文件描述符需要遍历fd数组,O(n)获得就绪的文件描述符需要遍历fd链表,O(n)当有就绪事件时,系统注册的回调函数就会被调用,将就绪的fd放入到就绪链表中。O(1)
FD数据拷贝每次调用select,需要将fd数据从用户空间拷贝到内核空间每次调用poll,需要将fd数据从用户空间拷贝到内核空间使用内存映射(mmap),不需要从用户空间频繁拷贝fd数据到内核空间
最大连接数有限制,一般为1024无限制无限制

1.5 其它IO函数

暂略

1.6 存储映射IO

暂略

6 进程间通信

对应APUE第十五章——进程间通信。

进程间通信(IPCInterProcess Communication)分为:

  • PIPE(管道)
  • Socket(套接字)
  • XSI(System V)
    • 消息队列
    • 信号量数组
    • 共享内存

这些手段都是用于进程间通讯的,只有进程间通讯才需要借助第三方机制,线程之间的通讯是不需要借助第三方机制的,因为线程之间的地址空间是共享的。

1. 管道

1.1 管道概述

1.1.1 概念

管道是UNIX系统IPC的最古老形式,所有UNIX系统都提供此种通信机制,管道有以下两种局限性。

  1. 历史上,它们是半双工的(即数据只能在一个方向上流动),现在,某些系统提供全双工管道。
  2. 管道只能在具有公共祖先的两个进程之间使用。通常,一个管道由一个进程创建,在进程调用fork之后,这个管道就能在父进程和子进程之间使用了。

尽管有这两种局限性,半双工管道仍是最常用的IPC形式。每当在管道中键入一个命令序列,让shell执行时,shell都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标准输出与后一条命令的标准输入相连接。

管道分为命名管道(FIFO)和匿名管道(PIPE),无论是哪种管道,都是由内核帮你创建和维护的。

本质是内核中的一块缓存区。

1.1.2 分类
  • 匿名管道:没有标识符,不能被其他进程找到,因此只能用于具有亲缘关系的进程间通信
  • 命名管道:有标识符,能被其他进程找到,因此可以用于同一主机上的任意进程间通信
1.1.3 管道特性
  • 管道中没有数据,继续读就会阻塞
  • 管道中数据满了,继续写就会阻塞
  • 管道中所有读端被关闭,继续写就会触发异常,导致程序退出
  • 管道中所有写端被关闭,继续读,则读取完管道中的数据后,将不再阻塞,而是返回0
1.1.4 命名管道独有特性
  • 若以只写方式打开管道文件,则会阻塞,直到管道被任意进程以读的方式打开
  • 若以只读方式打开管道文件,则会阻塞,直到管道被任意进程以写的方式打开
  • 因为一个管道如果不构成同时读写,就没有必要开辟缓存区
  • 匿名管道有的特性,命名管道也有

1.2 匿名管道

 匿名管道是通过调用pipe函数创建的。

#include <unistd.h>

int pipe(int pipefd[2]);

返回值:
    成功返回0
    失败返回-1

pipefd 是一个数组,表示管道的两端文件描述符,pipefd[0] 端作为读端,pipefd[1] 端作为写端。

pipe产生的是匿名管道,在磁盘的任何位置上找不到这个管道文件,而且匿名管道只能用于具有亲缘关系的进程之间通信。一般情况有亲缘关系的进程之间使用管道进行通信时,会把自己不用的一端文件描述符关闭。

注意:子进程打开的文件描述符与父进程的一致。

 代码示例

#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    int fd[2];
    pid_t pid;
    char buf[1024] = {"hello world"};
    

    //pipe,创建一个匿名管道(匿名管道看不到)
    if (pipe(fd) < 0)
    {
        perror("fail to pipe!");
        return -1;
    }

    fflush(NULL);
    pid = fork();
    if (pid < 0)
    {
        perror("fail to fork!");
        return -1;
    }
    if (pid == 0)//child
    {
        close(fd[0]);
        write(fd[1], buf, strlen(buf));//写进管道
        return 0;
    }
    else if (pid > 0)//parent
    {
        close(fd[1]);
        read(fd[0], buf, sizeof(buf));//从管道读
        write(1, buf, strlen(buf));//将读的数据从终端打印出来
        return 0;
    }

    return 0;
}

运行结果:

lei@ubuntu:~/Desktop/ipc$ ./a.out 
hello worldlei@ubuntu:~/Desktop/ipc$

1.3 命名管道mkfifo

mkfifo函数用于创建命名管道,作用与匿名管道相同,不过可以在不同的进程之间使用,相当于对一个普通文件进行读写操作就可以了。

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

int mkfifo(const char *pathname, mode_t mode);
  • pathname:管道文件的路径和文件名。
  • mode:创建管道文件的权限。该mode还需要和umask做并运算来确定最后的管道文件权限。
  • 返回值:成功返回 0,失败返回 -1 并设置 errno

当用mkfifo创建FIFO时,要用open来打开它。

FIFO有以下两种用途:

  • shell命令使用FIFO将数据从一条管道传送到另一条时,无需创建中间临时文件
  • 客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程二者之间传递数据。

管道必须凑齐读写双方才能正常运行

可以使用命令,来创建管道文件:

lei@ubuntu:~/Desktop/ipc$ mkfifo namefifo
lei@ubuntu:~/Desktop/ipc$ ls -l
total 4
prw-rw-r-- 1 lei lei   0 Oct 19 00:33 namefifo
-rw-rw-r-- 1 lei lei 781 Oct 19 00:30 pipe.c

由第一位p可知其为一个管道文件。

将时间重定向到命名管道中:

lei@ubuntu:~/Desktop/ipc$ date
Thu Oct 19 00:33:46 PDT 2023
lei@ubuntu:~/Desktop/ipc$ date > namefifo 

可以看到shell执行在这步没有结束,原因是我们重定向属于打开命名管道的写入端,但是命名管道必须打开读写两端才可以执行下去。

我们打开读端:

lei@ubuntu:~/Desktop/ipc$ cat namefifo 
Thu Oct 19 00:34:28 PDT 2023
lei@ubuntu:~/Desktop/ipc$ 

可以看到读端直接结束 

lei@ubuntu:~/Desktop/ipc$ date > namefifo 
lei@ubuntu:~/Desktop/ipc$ 

写段也结束了

lei@ubuntu:~/Desktop/ipc$ ls -l
total 4
prw-rw-r-- 1 lei lei   0 Oct 19 00:33 namefifo
-rw-rw-r-- 1 lei lei 781 Oct 19 00:30 pipe.c

再次观察到namefifo文件大小依旧是0,因为他只是一个名字,具体数据在内核的缓存区中。

(管道文件只是一个标识符,一个名字,只有在打开的时候,才会开辟管道空间。)


代码示例1-client向server发送数据 

head.h 

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

#define MY_FIFO "./fifo"

server.c 

#include "head.h"

#define BUFFSIZE 10

int main(int argc, char const *argv[])
{
    umask(0002);

    if (mkfifo(MY_FIFO, 0664) < 0)
    {   
        perror("faill to fifo");
        return -1; 
    }   

    int fd = open(MY_FIFO, O_RDONLY);
    if (fd < 0)
    {   
        perror("fail to open");
        return -1; 
    }   

    while(1)
    {   
        char buf[BUFFSIZE];
    
        memset(buf, 0, sizeof(buf));

        //从管道读取数据
        ssize_t size = read(fd, buf, BUFFSIZE - 1); 
        if (size < 0)
        {
            perror("read error");
            break;
        }
        else if (size == 0)
        {
            printf("client quit\n");
            break;
        }
        else
        {
            printf("client -> %s\n", buf);
        }
    }   

    close(fd);
    
    return 0;
}

 client.c

#include "head.h"

#define BUFFSIZE 10

int main(int argc, char const *argv[])
{
    int fd = open(MY_FIFO, O_WRONLY);
    if (fd < 0)
    {   
        perror("fail to open");
        return -1; 
    }   

    while(1)
    {   
        printf("请输入-> ");
        fflush(stdout);
        char buf[BUFFSIZE];

        //从标准输入端读取buffsize-1个数据放buf,,read会读取‘\n’
        ssize_t s = read(0, buf, BUFFSIZE  - 1); 
        if(s > 0) {
            if (buf[s-1] == '\n')
            {
                buf[s-1] = '\0';
            }
            else 
            {
                buf[s] = '\0';
            }
 //           printf("buf = %s,strlen(buf) = %d\n", buf,strlen(buf));
            write(fd, buf, strlen(buf));//写入管道
        }
    
    }   

    close(fd);
    
    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/ipc/fifo$ ./server 
client -> hello
client -> hello wor
client -> ld
client -> i love yo
client -> u
client -> nihao
client -> hi
client quit
lei@ubuntu:~/Desktop/ipc/fifo$ ./client 
请输入-> hello
请输入-> hello world
请输入-> 请输入-> i love you
请输入-> 请输入-> nihao
请输入-> hi
请输入-> ^C

代码示例2-使用fifo进行客户端-客户端进行通信

head.h 

#ifndef __HEAD_H__
#define __HEAD_H__

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

#endif

 client1.c

#include "head.h"

int main()
{
    int fdwtor = 0;
    int fdrtow = 0;
    int ret = 0;
    char buf[4096] = {0};

    mkfifo("wtor", 0664);
    mkfifo("rtow", 0664);
    
    fdwtor = open("wtor", O_WRONLY);
    if(-1 == fdwtor)
    {
        perror("fail to open wtor");
        return -1;
    }
    fdrtow = open("rtow", O_RDONLY);
    if(fdrtow < 0)
    {
        perror("fail to open rtow");
        return -1;
    }
    
    //写,读 
    while(1)
    {
        fgets(buf, sizeof(buf), stdin);
	    buf[strlen(buf)-1] = '\0';//fgets会将终端\n接受,将\n换成\0
        write(fdwtor, buf, strlen(buf));
        memset(buf, 0, sizeof(buf));
        read(fdrtow, buf, sizeof(buf));
        printf("RECV:%s\n", buf);
    }

    close(fdwtor);
    close(fdrtow);

    return 0;
}

client2.c 

#include "head.h"

int main()
{
    int fdwtor = 0;
    int fdrtow = 0;
    int ret = 0;
    char buf[4096] = {0};

    mkfifo("wtor", 0664);
    mkfifo("rtow", 0664);
    
    fdwtor = open("wtor", O_RDONLY);
    if(-1 == fdwtor)
    {
        perror("fail to open wtor");
        return -1;
    }
    fdrtow = open("rtow", O_WRONLY);
    if(fdrtow < 0)
    {
        perror("fail to open rtow");
        return -1;
    }
    
    //读,写  
    while(1)
    {
        memset(buf, 0, sizeof(buf));
        read(fdwtor, buf, sizeof(buf));
        printf("RECV:%s\n", buf);
        gets(buf);
        write(fdrtow, buf, strlen(buf));
    }

    close(fdwtor);
    close(fdrtow);

    return 0;
}

 执行结果:

lei@ubuntu:~/Desktop/ipc/fifochat$ ./client1
hello
RECV:nihao
nihaoya
lei@ubuntu:~/Desktop/ipc/fifochat$ ./client2
RECV:hello
nihao
RECV:nihaoya

分析:程序只能client1写->client2读->client2写->client1读->client1写循环,不能做到随时写立即读(连续发送)解决方法使用线程将读写分离。

代码示例3-使用fifo+pthread进行客户端-客户端进行通信

client1.c 

#include "head.h"

int fatob;
int fbtoa;

void *sendfun(void *arg)
{
    char buf[4096] = {0};

    while(1)
    {
        memset(buf, 0, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf)-1] = '\0';
        write(fatob, buf, strlen(buf));
        if(!strcmp(buf, "quit"))
        {
            break;
        }
    }

    return NULL;
}

void *recvfun(void *arg)
{
    char buf[4096] = {0};

    while(1)
    {
        memset(buf, 0, sizeof(buf));
        read(fbtoa, buf, sizeof(buf));
        if(!strcmp(buf, "quit"))
        {
            break;
        }
        printf("RECV:%s\n",buf);
    }

    return NULL;
}


int main()
{
    pthread_t tid_send;
    pthread_t tid_recv;

    mkfifo("a", 0664);
    mkfifo("b", 0664);

    fatob = open("a", O_WRONLY);
    fbtoa = open("b", O_RDONLY);
    if (-1 == fatob || -1 == fbtoa)
    {
        perror("fail to open");
        return -1;
    }
    pthread_create(&tid_send, NULL, sendfun, NULL);
    pthread_create(&tid_recv, NULL, recvfun, NULL);

    pthread_join(tid_send, NULL);
    pthread_join(tid_recv, NULL);

    return 0;
}

 client2.c

#include "head.h"

int fatob;
int fbtoa;

void *sendfun(void *arg)
{
    char buf[4096] = {0};

    while(1)
    {
        memset(buf, 0, sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf)-1] = '\0';
        write(fbtoa, buf, strlen(buf));
        if(!strcmp(buf, "quit"))
        {
            break;
        }
    }

    return NULL;
}

void *recvfun(void *arg)
{
    char buf[4096] = {0};

    while(1)
    {
        memset(buf, 0, sizeof(buf));
        read(fatob, buf, sizeof(buf));
        if(!strcmp(buf, "quit"))
        {
            break;
        }
        printf("RECV:%s\n",buf);
    }

    return NULL;
}


int main()
{
    pthread_t tid_send;
    pthread_t tid_recv;

    mkfifo("a", 0664);
    mkfifo("b", 0664);

    fatob = open("a", O_RDONLY);
    fbtoa = open("b", O_WRONLY);
    if (-1 == fatob || -1 == fbtoa)
    {
        perror("fail to open");
        return -1;
    }

    pthread_create(&tid_send, NULL, sendfun, NULL);
    pthread_create(&tid_recv, NULL, recvfun, NULL);

    pthread_join(tid_send, NULL);
    pthread_join(tid_recv, NULL);

    return 0;
}

 运行结果:

lei@ubuntu:~/Desktop/ipc/fifochat$ ./client1
RECV:hello
RECV:world
ni
hao
quit
lei@ubuntu:~/Desktop/ipc/fifochat$ ./client2
hello
world
RECV:ni
RECV:hao
quit

2. XSI IPC

XSI IPC函数是紧密地基于System V的IPC函数的。

system V:同一主机内的进程间通信方案,在OS层面专门为进程间通信设计的方案。

system V标准下的三种通信方式:

  • 共享内存
  • 消息队列
  • 信号量

2.1 相关命令

  • ipcs命令可以查看 XSI IPC 的使用情况。
  • ipcrm命令可以删除指定的 XSI IPC。
lei@ubuntu:~$ ipcs

------ Message Queues -------- #消息队列
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------  #共享内存
key        shmid      owner      perms      bytes      nattch     status              

------ Semaphore Arrays --------  #信号量数组
key        semid      owner      perms      nsems

通过 ipcs 命令可以看出来,命令的输出结果分为三个部分,第一部分是系统中当前开辟的共享内存(shm),第二部分是信号量数组(sem),第三部分是消息队列(msg)

可以看到,不论是哪一部分,都有一列叫做key,使用 XSI IPC 通信的进程就是通过同一个 key 值操作同一个共享资源的。这个 key 是一个正整数,与文件描述符不同的是,生成一个新 key 值时它不采用当前可用数值中的最小值,而是类似生成进程 ID 的方式,key 值连续的加 1,直至达到一个整数的最大正值,然后再回转到 0 从头开始累加。

  • ipcs -l 查看IPC相关的限制 
lei@ubuntu:~/Desktop/ipc/fifo$ ipcs -l

------ Messages Limits --------
max queues system wide = 32000
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384

------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398509481980
min seg size (bytes) = 1

------ Semaphore Limits --------
max number of arrays = 32000
max semaphores per array = 32000
max semaphores system wide = 1024000000
max ops per semop call = 500
semaphore max value = 32767

 不同IPC的特征比较

 

2.2 标识符和键

每个内核中的IPC结构(消息队列、信号量或共享存储)都用一个非负整数的标识符(identifier)加以引用。例如,要向一个消息队列发送消息或者从一个消息队列取消息,只需要知道其队列标识符。标识符是IPC对象的内部名,为使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。为此,每个 IPC对象都与一个(key)相关联,将这个键作为该对象的外部名。

无论何时创建IPC结构(通过调用msgget、semget或shmget 创建),都应指定一个键。这个键的数据类型是基本系统数据类型key_t,通常在头文件<sys/types.h>中被定义为长整型。这个键由内核变换成标识符

一个IPC对象可以同时有消息队列、信号量或共享存储各一个。

有多种方法使客户进程和服务器进程在同一IPC结构上汇聚。

  • 方法1:服务器进程可以指定键IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。键IPC_PRIVATE保证服务器进程创建一个新IPC结构。这种技术的缺点是:文件系统操作需要服务器进程将整型标识符写到文件中,此后客户进程又要读这个文件取得此标识符。IPC_PRIVATE键也可用于父进程子关系。父进程指定IPC_PRIVATE创建一个新IPC结构,所返回的标识符可供fork后的子进程使用。接着,子进程又可将此标识符作为exec函数的一个参数传给一个新程序。

  • 方法2:可以在一个公用头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。这种方法的问题是该键可能已与一个IPC结构相结合,在此情况下,get 函数(msgget、semget 或shmget)出错返回。服务器进程必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。

  • 方法3:客户进程和服务器进程认同一个路径名和项目ID(项目ID是0~255之间的字符值),接着,调用函数ftok将这两个值变换为一个键。然后在方法2中使用此键。ftok提供的唯一服务就是由一个路径名和项目ID产生一个键。
#include <sys/ipc.h>

key_t ftok(const chat *path, int id);

返回值:
    成功返回键(IPC对象名)
    失败返回(key_t)-1

path参数必须引用一个现有的文件。当产生键时,只使用id参数的低8位。8位id,例如key = 0x2345678;或key = ‘a’;

对于不同文件的两个路径名,如果使用同一项目ID,可能产生相同的键。

3个get 函数(msgget、semget和shmget)都有两个类似的参数:一个key和一个整型flag。在创建新的IPC结构(通常由服务器进程创建)时,如果key是IPC_PRIVATE或者和当前某种类型的IPC结构无关,则需要指明 flag 的IPC_CREAT标志位。为了引用一个现有队列(通常由客户进程创建),key必须等于队列创建时指明的key的值,并且IPC_CREAT必须不被指明。

注意,决不能指定IPC_PRIVATE作为键来引用一个现有队列,因为这个特殊的键值总是用于创建一个新队列。

如果希望创建一个新的IPC结构,而且要确保没有引用具有同一标识符的一个现有IPC结构,那么必须在flag中同时指定IPC_CREATIPC_EXCL位。这样做了以后,如果IPC结构已经存在就会造成出错,返回EEXTST

2.3 消息队列

消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。

本质:内核中的一个优先级队列

msg、sem 和 shm 都有一系列函数遵循下面的命名规则:

  • xxxget():创建或引用,将key转换为标识符id
  • xxxop():相关操作
  • xxxctl():其它的控制或销毁

创建每一个队列都会有 一个msqid_ds结构体与之关联:

struct msqid_ds {
    struct ipc_perm msg_perm; // 保存ipc权限信息的结构体
    msgqnum_t msg_qnum;
    msglen_t msg_qbytes;
    pid_t msg_lspid;
    pid_t msg_lrpid;
	// ...
}

相关系统调用

// msgget - get a System V message queue identifier

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

// 函数的作用是创建或引用一个消息队列,消息队列是双工的,两边都可以读写。
int msgget(key_t key, int msgflg);
  • key:IPC内核标识符的外部方案实现,拥有相同 key 的双方才可以通信。key 值必须是唯一的,ftok 函数可以用于获取 key
  • msgflg:IPC_CREAT  没有就创建   IPC_EXCL  如果之前已经创建了消息队列,会报错
  • 返回:非负的队列ID或出错-1
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

// 将 msgp 指向的结构体存放到 msgid 的消息队列中,这段空间有 msgz 个字节大小,msgz 的值要减掉强制的成员 mtype 的大小(sizeof(long))。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

// 从 msgid 这个消息队列中取出 msgp 结构体数据,msgp 的大小是 msgsz,msgflg 是特殊要求,没有特殊要求可以写 0。 
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
               int msgflg);
// msgtyp可以指定接收哪一种信息“
// 0:  返回队列的第一个信息
// >0: 返回队列中消息类型为 type 的第一个消息。
// <0: 返回队列中消息类型值小于等于 type 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。


/* msgp 指向的结构体的成员定义要类似 msgbuf 这个结构体,第一个成员必须是 long 类型的 mtype,并且必须是 > 0 的值 */
struct msgbuf {
    long mtype;       /* 消息类型,必须 > 0 */
    char mtext[512];  /* 消息数据字段 */
};

msgp 指向的结构体好处是可以实现根据消息类型拿所需要的数据。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
// msqctl函数对队列执行多种操作。它和另外两个与信号量及共享存储有关的函数(semctl 和shmctl)都是XSI IPC的类似于ioctl的函数(亦即垃圾桶函数)。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

cmd参数指定对msqid指定的队列要执行的命令:

  • IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中。
  • IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在它们下一次试图对此队列进行操作时,将得到EIDRM错误。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。
  • IPC_SET:设置消息队列权限

返回值:

  • 成功返回0
  • 失败返回-1 

 代码示例1-单向通信(msgp = char*)

send.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;
    char buf[1024] = {0};

    //与接收方相同的文件和id才能生成相同的key
    key = ftok("./ipcfile", 100);
    if (key == -1)
    {
        perror("fail to ftok!");
        return -1;
    }

    //获取消息队列
    msgid = msgget(key, IPC_CREAT|0664);
    if (msgid == -1)
    {
        perror("fail to msgget!");
        return -1;
    }
    
    while(1)
    { 
        memset(buf, 0, sizeof(buf));
        fgets(buf, sizeof(buf),stdin);//会接收‘\n’
  
        int size = msgsnd(msgid, &buf, strlen(buf), 0);
        if (size < 0)
        {
            perror("fail to msgsnd!");
            return -1;
        }
        printf("buf = %s", buf);
    }

    //消息队列不是发送方创建的,所以不需要负责销毁 msgctl(msgid, IPC_RMID, NULL);

    return 0;
}

recv.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;
    char buf[1024] = {0};
    
    //通过文件和id生成唯一的key
    key = ftok("./ipcfile", 100);
    if (key == -1)
    {
        perror("fail to ftok!");
        return -1;
    }
    
    接收端应该先启动,所以消息队列由接收端创建
    msgid = msgget(key, IPC_CREAT|0664);
    if (msgid == -1)
    {
        perror("fail to msgget!");
        return -1;
    }

    while(1)
    {    
        memset(buf, 0, sizeof(buf));
        
        //没有消息时候会阻塞
        ssize_t size = msgrcv(msgid, &buf, sizeof(buf), 0, 0);
        if (size < 0)
        {
            perror("fail to msgrcv!");
            return -1;
        }       
        printf("buf = %s", buf);

    }

    //谁创建谁销毁
    msgctl(msgid, IPC_RMID, NULL);

    return 0;
}

执行结果:

lei@ubuntu:~/Desktop/ipc/msg$ gcc send.c -o send
lei@ubuntu:~/Desktop/ipc/msg$ ./send 
hello
buf = hello
how
buf = how
are
buf = are
you
buf = you
lei@ubuntu:~/Desktop/ipc/msg$ gcc recv.c -o recv
lei@ubuntu:~/Desktop/ipc/msg$ ./recv 
buf = hello
buf = how
buf = are
buf = you

如果不开启接收端,发送端发送的数据会保存在缓存中,等待接收端开启,一次性将数据接收。

缓存区的大小是有所限制的, POSIX message queues     (bytes, -q) 819200 

lei@ubuntu:~/Desktop/ipc$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15400
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15400
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

可以使用ulimit -q 改变缓存区大小。

代码示例2-单向通信(msgp= struct msgbuf *)

 send.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

/* 通讯双方约定的协议 */
struct msgbuf{
    long mtype; // 消息类型
    char name[32]; // 消息数据
    int math; // 其他信息
};

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;
    struct msgbuf buf;
    
    key = ftok("./ipcfile", 'a');
    if (key == -1)
    {
        perror("fail to ftok!");
        return -1;
    }

    msgid = msgget(key, IPC_CREAT | 0664);
    if (msgid == -1)
    {
        perror("fail to msgget!");
        return -1;
    }
    
    while(1)
    { 
        memset(&buf, 0, sizeof(buf));

        buf.mtype = 100;
        buf.math = 49;
        fgets(buf.name, sizeof(buf.name),stdin);//会接收‘\n’
        buf.name[strlen(buf.name)-1] = '\0';
  
        int size = msgsnd(msgid, &buf, sizeof(buf), 0);
        if (size < 0)
        {
            perror("fail to gcmsgsnd!");
            return -1;
        }
    }

    return 0;
}

 recv.c

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

/* 通讯双方约定的协议 */
struct msgbuf{
    long mtype; // 消息类型
    char name[32]; // 消息数据
    int math; // 其他信息
};

int main(int argc, char const *argv[])
{
    key_t key;
    int msgid;
    struct msgbuf buf;
    
    key = ftok("./ipcfile", 'a');
    if (key == -1)
    {
        perror("fail to ftok!");
        return -1;
    }

    msgid = msgget(key, IPC_CREAT|0664);
    if (msgid == -1)
    {
        perror("fail to msgget!");
        return -1;
    }

    while(1)
    {    
        memset(&buf, 0, sizeof(buf));

        ssize_t size = msgrcv(msgid, &buf, sizeof(buf), 100, 0);
        if (size < 0)
        {
            perror("fail to msgrcv!");
            return -1;
        }       
        printf("buf.mytype = %ld, ", buf.mtype);
        printf("buf.math = %d, ", buf.math);
        printf("buf.name = %s\n", buf.name);
    }


    msgctl(msgid, IPC_RMID, NULL);

    return 0;
}

执行结果: 

lei@ubuntu:~/Desktop/ipc/msg$ ./send 
wanglei
xiaoming
lei@ubuntu:~/Desktop/ipc/msg$ ./recv 
buf.mytype = 100, buf.math = 49, buf.name = wanglei
buf.mytype = 100, buf.math = 49, buf.name = xiaoming

代码示例3-双向通信(加入pthread) 

2.4 共享内存

作用:用于多个进程之间的数据共享

特性:最快的进程间通信方式,生命周期随内核(并不会随着打开的进程退出而释放)

原理:开辟一块物理内存,然后多个进程将这块内存都通过页表映射到自己的虚拟地址空间中,通过虚拟地址直接访问物理内存中数据。(管道是将进程A的数据拷贝到管道缓存区,进程B从缓存区将数据拷贝到自己的地址空间。共享内存相较于没有两次拷贝过程,所以最快)

使用共享存储时要掌握的唯一窍门是,在多个进程之间同步访问一个给定的存储区。若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据。通常,信号量用于同步共享存储访问。(也可以用记录锁或互斥量)

我们已经看到了共享存储的一种形式,就是在多个进程将同一个文件映射到它们的地址空间的时候。XSI共享存储和内存映射的文件的不同之处在于,前者没有相关的文件。XSI共享存储段是内存的匿名段

相关系统调用 

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

int shmget(key_t key, size_t size, int shmflg);

返回值:
    成功返回共享内存ID
    失败返回-1

 shmget获得一个共享内存 

参数:

key:IPC对象名

size:共享内存空间大小,实现通常将其向上取为系统页长(4096个字节)的整数倍

shmflg: IPC_CREAT |  IPC_EXECL | 0664

  • IPC_CREAT  若果不存在则创建并打开,存在则打开
  • IPC_EXECL  与IPC_CREAT搭配使用,共享内存不存在则创建打开,若存在则报错返回
  • mode_flags  共享内存的访问权限 0664 
#include <sys/types.h>
#include <sys/shm.h>

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

返回值:    
     成功返回映射到共享内存空间中的地址
     失败返回-1     

shmat 将进程空间地址映射到共享内存 

参数:
shmid:共享内存ID号 
shmaddr:
            NULL    表示系统选择一个合适的地址映射                       

int shmdt(const void *shmaddr);

返回值:
     成功返回0 
     失败返回-1 

shmdt解除映射  

参数:
        shmaddr:共享内存空间的映射首地址,也是shmat的返回值

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

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

返回值:
      成功返回0 
      失败返回-1 

shmctl向共享内存中发送一条命令 

参数:
shmid:共享内存ID号 
cmd:
        IPC_STAT 
        IPC_SET 
        IPC_RMID    删除共享内存 
buf: 用于设置获取共享内存信息,当cmd是IPC_RMID 时被忽略
       NULL

shmctl的RMID只是删除标记共享内存需要删除,这时候并不会立即删除共享内存,而是拒绝后续新的映射连接,当映射连接为0的时候则有系统完成删除。

代码示例-单向通信

 read.c

#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_KEY 0x123456789

int main(int argc, char const *argv[])
{
    key_t key = 0;

    key = ftok("./", SHM_KEY);
    if (key == -1)
    {
        perror("fail to ftok");
        return -1;
    }

    int shmid = shmget(SHM_KEY, 4096, IPC_CREAT | 0664);
    if (shmid < 0)
    {
        perror("fail to shmget");
        return -1;
    }

    void *pstart = shmat(shmid, NULL, 0);
    if (pstart == NULL)
    {
        perror("fail to shmat");
        return -1;
    }

    while(1)
    {
        printf("%s\n", (char *)pstart);
        sleep(1);
    }

    shmdt(pstart);
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

write.c 

#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_KEY 0x123456789

int main(int argc, char const *argv[])
{
    key_t key = 0;

    key = ftok("./", SHM_KEY);
    if (key == -1)
    {
        perror("fail to ftok");
        return -1;
    }

    int shmid = shmget(SHM_KEY, 4096, IPC_CREAT | 0664);
    if (shmid < 0)
    {
        perror("fail to shmget");
        return -1;
    }

    void *pstart = shmat(shmid, NULL, 0);
    if (pstart == (void *)-1)
    {
        perror("fail to shmat");
        return -1;
    }

    while(1)
    {
        char buf[4096] = {0};
        gets(buf);
        sprintf(pstart,"hello world%s", buf);
        sleep(1);
    }

    shmdt(pstart);
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

执行结果: 

lei@ubuntu:~/Desktop/ipc/shm$ ./write 
nihao
hello
lei@ubuntu:~/Desktop/ipc/shm$ ./read 
hello worldad
hello worldad
hello worldad
hello worldad
hello worldad
hello worldnihao
hello worldnihao
hello worldnihao
hello worldnihao
hello worldhello
hello worldhello
hello worldhello

分析:读取端会一直读取内存中的数据,并不能实现同步。

解决方法:使用互斥量或信号量解决共享内存访问问题。(6.2.5中代码示例二)

2.5 信号量

信号量与已经介绍过的IPC(管道、FIFO、消息列队、共享内存)不同。它是一个计数器,用于为多个进程提供对共享数据对象的访问。

为了获得共享资源,进程需要执行下列操作:

  1. 测试控制该资源的信号量。
  2. 若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减1,表示它使用了一个资源单位。
  3. 否则,若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。进程被唤醒后,它返回至步骤1。

当进程不再使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒它们。

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。

常用的信号最形式被称为二元信号量(binary semaphore)。它控制单个资源,其初始值为1。但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用。

相关系统调用

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

// 创建或获得一个信号量id
int semget(key_t key, int nsems, int semflg);
  • key:键值,具有亲缘关系的进程之间可以使用一个匿名的 key 值,key 使用宏 IPC_PRIVATE 即可。
  • nsems:表示有多少个信号。信号量实际上是一个计数器,所以如果设置为 1 可以用来模拟互斥量。
  • semflgIPC_CREAT 表示创建信号量,同时需要按位或一个权限,如果是匿名 IPC 则无需指定这个宏,直接给权限就行了。
  • 成功返回 sem ID,失败返回 -1 并设置 errno。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 用来控制或销毁信号量
int semctl(int semid, int semnum, int cmd, ...);

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) */
           };
  • semid:信号量id
  • semnum:信号量数组的下标
  • cmd:
            IPC_STAT 
            IPC_SET 
            IPC_RMID    删除信号量
           SETVAL  设置初值,必须声明共用体semun
  • 成功返回0,失败返回-1
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

// 操作信号量(申请和释放)
int semop(int semid, struct sembuf *sops, unsigned nsops);

struct sembuf {
    unsigned short sem_num; /* 对第几个资源(数组下标)操作 */
    short sem_op; /* 取几个资源写负数几(不要写减等于),归还几个资源就写正数几 */
    short sem_flg; /* 特殊要求 */
};
  • sops:申请释放操作信号量的数组空间首地址结构体数组起始位置;
  • nsops:结构体数组长度;
  • 返回值:成功返回0,失败返回-1并设置 errno。

代码示例1-

#include <sys/sem.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <unistd.h>

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) */
};

int main(int argc, char const *argv[])
{
    key_t key = 0;

    union semun myun;
    struct sembuf mybuf;
    
    //创建IPC对象
    key = ftok("/", 'a');
    if (key == -1)
    {
        perror("fail to ftok");
        return -1;
    }

    //创建两个信号量,读,写
    int semid = semget(key, 2, IPC_CREAT | 0664);
    if (semid == -1)
    {
        perror("fail to semget");
        return -1;
    }
    
    //设置读信号量0,初始值0
    myun.val = 0;
    int ret = semctl(semid, 0, SETVAL, myun);
    if (ret == -1)
    {
        perror("fail to semctl");
        return -1;
    }

    //设置写信号量1,初始值1
    myun.val = 1;
    ret = semctl(semid, 1, SETVAL, myun);
    if (ret == -1)
    {
        perror("fail to semctl");
        return -1;
    }

    /*申请写资源*/
    mybuf.sem_num = 1;//信号量1
    mybuf.sem_op = -1;//操作减一(申请)
    mybuf.sem_flg = SEM_UNDO;//操作结束啥都不干
    ret = semop(semid, &mybuf, 1);
    if (ret == -1)
    {
        perror("fail to semop");
        return -1;
    }
    printf("申请到写资源!\n");

    return 0;
}

运行结果:

lei@ubuntu:~/Desktop/ipc/sem$ ./a.out 
申请到写资源!
------ Semaphore Arrays --------
key        semid      owner      perms      nsems     
0x61010002 6          lei        664        2 

如果再次申请写资程序会阻塞。

必须释放一次,申请一次,最后删除信号量

    /*释放写资源*/
    mybuf.sem_num = 1;//信号量1
    mybuf.sem_op = 1;//操作+1(申请)
    mybuf.sem_flg = SEM_UNDO;//操作结束啥都不干
    ret = semop(semid, &mybuf, 1);
    if (ret == -1)
    {
        perror("fail to semop");
        return -1;
    }
    printf("释放写资源!\n");

    /*释放写资源*/
    mybuf.sem_num = 1;//信号量1
    mybuf.sem_op = -1;//操作减一(申请)
    mybuf.sem_flg = SEM_UNDO;//操作结束啥都不干
    ret = semop(semid, &mybuf, 1);
    if (ret == -1)
    {
        perror("fail to semop");
        return -1;
    }
    printf("申请到写资源!\n");

    semctl(semid, 1, IPC_RMID);//删除信号量

执行结果:

lei@ubuntu:~/Desktop/ipc/sem$ ./a.out 
申请到写资源!
释放写资源!
申请到写资源!
lei@ubuntu:~/Desktop/ipc/sem$ ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
       
------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

代码示例2-使用信号量使共享内存同步

head.h

#ifndef __HEAD_H__
#define __HEAD_H__

#include <sys/sem.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>

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) */
};

extern int sem_init(int semid, int *parray, int len);
extern int sem_p(int semid, int num);
extern int sem_v(int semid, int num);

#endif

sem.c

#include "head.h"

/*初始化信号量的值*/
int sem_init(int semid, int *parray, int len)
{
    int i = 0;
    int ret = 0;
    union semun myun;

    for(i = 0; i < len; i++)
    {
        myun.val = parray[i];
        ret = semctl(semid, i, SETVAL, myun);
        if (ret == -1)
        {
            perror("fail to semctl");
            return -1;
        }
    }

    return 0;
}

/*申请信号量*/
int sem_p(int semid, int num)
{
    struct sembuf mybuf;
    int ret = 0;

    mybuf.sem_flg = SEM_UNDO;
    mybuf.sem_num = num;
    mybuf.sem_op = -1;
    ret = semop(semid, &mybuf, 1);
    if (ret == -1)
    {
        perror("fail to semop");
        return -1;
    }

    return 0;
}

/*释放信号量*/
int sem_v(int semid, int num)
{
    struct sembuf mybuf;
    int ret = 0;

    mybuf.sem_flg = SEM_UNDO;
    mybuf.sem_num = num;
    mybuf.sem_op = +1;
    ret = semop(semid, &mybuf, 1);
    if (ret == -1)
    {
        perror("fail to semop");
        return -1;
    }

    return 0;
}

write.c

#include "head.h"

int main(int argc, char const *argv[])
{
    key_t key;
    int shmid = 0;
    int semid = 0;
    int array[2] = {0, 1};

    key = ftok("/", 'a');
    if (key == -1)
    {
        perror("fail to ftok");
        return -1;
    }

    /*获取共享内存*/
    shmid = shmget(key, 4096, IPC_CREAT | 0664);
    if (shmid == -1)
    {
        perror("fail to shmget");
        return -1;
    }
    /*获取2个信号量*/
    semid = semget(key, 2, IPC_CREAT | 0664);
    if (semid == -1)
    {
        perror("fail to semget");
        return -1;
    }
    /*初始化信号量*/
    sem_init(semid, array, 2);

    /*申请映射空间,并且映射*/
    char *pshmaddr = (void *)shmat(shmid, NULL, 0);
    if (pshmaddr == NULL)
    {
        perror("fail to shmat");
        return -1;
    }

    while(1)
    {
        sem_p(semid, 1);//申请写
        gets(pshmaddr);
        sem_v(semid, 0);//释放读
        if (!strcmp(pshmaddr, "quit"))
        {
            break;
        }
    }

    shmdt(pshmaddr);//解除映射
    shmctl(shmid, IPC_RMID, NULL);//删除共享内存
    semctl(semid, 0, IPC_RMID);//删除信号量

    return 0;
}

read.c

#include "head.h"

int main(int argc, char const *argv[])
{
    key_t key;
    int shmid = 0;
    int semid = 0;
    int array[2] = {0, 1};

    key = ftok("/", 'a');
    if (key == -1)
    {
        perror("fail to ftok");
        return -1;
    }

    /*获取共享内存*/
    shmid = shmget(key, 4096, IPC_CREAT | 0664);
    if (shmid == -1)
    {
        perror("fail to shmget");
        return -1;
    }
    /*获取2个信号量*/
    semid = semget(key, 2, IPC_CREAT | 0664);
    if (semid == -1)
    {
        perror("fail to semget");
        return -1;
    }
    /*初始化信号量*/
    sem_init(semid, array, 2);

    /*申请映射空间,并且映射*/
    char *pshmaddr = (char *)shmat(shmid, NULL, 0);
    if (pshmaddr == NULL)
    {
        perror("fail to shmat");
        return -1;
    }

    while(1)
    {
        sem_p(semid, 0);//申请读
        printf("SHMADDR:%s\n", pshmaddr);
        if (!strcmp(pshmaddr, "quit"))
        {
            break;
        }
        sem_v(semid, 1);//释放写
    }

    shmdt(pshmaddr);//解除映射
    shmctl(shmid, IPC_RMID, NULL);//删除共享内存
    semctl(semid, 0, IPC_RMID);//删除信号量

    return 0;
}

makefile

all:write read

read:read.c sem.c
	gcc $^ -o $@
write:write.c sem.c
	gcc $^ -o $@

.PHONY:
clean:
	rm read write

执行结果:

ei@ubuntu:~/Desktop/ipc/shm-sem$ ./write 
hello 
world
hi my friend
quit
lei@ubuntu:~/Desktop/ipc/shm-sem$ 
lei@ubuntu:~/Desktop/ipc/shm-sem$ ./read 
SHMADDR:world
SHMADDR:hi my friend
SHMADDR:quit
lei@ubuntu:~/Desktop/ipc/shm-sem$ 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值