Linux系统编程(一)

文件I/O

文件描述符

所有执行I/O 操作的系统调用都以文件描述符,一个非负整数(通常是小整数),来指代打开的文件。文件描述符用以表示所有类型的已打开文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。针对每个进程,文件描述符都自成一套。

文件描述符 用途 POSIX名称 stdio流
0 标准输入 STDIN_FILENO stdin
1 标准输出 STDOUT_FILENO stdout
2 标准错误 STDERR_FILENO stderr

UNIX I/O 模型的显著特点之一是其输入/输出的通用性概念。这意味着使用4个同样的系统调用open()、read()、write()和close()可以对所有类型的文件执行I/O 操作,包括终端之类的设备。

open()

open()调用既能打开一个业已存在的文件,也能创建并打开一个新文件。

# include <sys/stat.h>
# include <fcntl.h>
int open(const char *pathname, int flags, .../* mod_t mode */)

pathname,文件目录
flags,打开形式
如果调用成功,open()将返回一文件描述符,用于在后续函数调用中指代该文件。
若发生错误,则返回−1,并将errno 置为相应的错误标志。

flags:

标志 用途 统一UNIX规范版本
O_RDONLY 以只读形式打开 v3
O_WRONLY 以只写方式打开 v3
O_RDWR 以读写方式打开 v3
O_CLOEXEC 设置close-on-exec标志 v4
O_CREAT 若文件不存在,则创建文件 v3
O_DIRECT 无缓冲的输入/输出
O_DIRECTORY 如果pathname不是目录,则失败 v4
O_EXCL 结合O_CREAT参数使用,专门用于创建文件 v3
O_LARGEFILE 在32位系统中打开大文件
O_NOATIME 调用read()时,不修改文件最近访问时间(Linux2.6.8开始)
O_NOCTTY 不要让pathname成为控制终端 v3
O_NOFOLLOW 对符号链接不予解引用 v4
O_TRUNC 截断已有文件,使其长度为0 v3
O_APPEND 在文件尾部追加数据 v3
O_ASYNC 当I/O操作可行时,产生信号通知进程
O_DSYNC 提供同步的I/O数据完整性(自Linux2.6.33版本开始) v3
O_NONBLOCK 以非阻塞方式打开 v3
O_SYNC 以同步方式写入文件 v3

open()常用错误码:

错误码 解释
EACCES 文件权限不允许以flags参数指定的方式打开文件。
EISDIR 要打开的文件是一个目录,通常情况下不允许对目录进行写操作。
ENFILE 文件打开数量已经达到系统允许的上限。
ENOENT 文件不存在,并且未指定O_CREAT标志。
EROFS 文件是只读的,调用者企图用写的方式打开。
ETXTBSY 要打开的文件是正在运行的程序文件。系统不允许修改正在运行的程序。

mode_t详解:

S_IRWXU  00700 属主读、写、执行
S_IRUSR  00400 属主读
S_IWUSR  00200 属主写
S_IXUSR  00100 属主执行
S_IRWXG  00070 属组读、写、执行
S_IRGRP  00040 属组读
S_IWGRP  00020 属组写
S_IXGRP  00010 属组执行
S_IRWXO  00007 其他读、写、执行
S_IROTH  00004 其他读
S_IWOTH  00002 其他写
S_IXOTH  00001 其他执行
S_ISUID  0004000 把进程的有效用户设置为文件的所有者
S_ISGID  0002000 把进程的有效组设置为文件的组
S_ISVTX  0001000 粘着位,作用1:可执行程序执行结束后,会缓存到交换空间,由于交换空间的磁盘是连续的,不需要找block,所以可以实现快速加载程序的效果。作用2:用来标记目录,标记后的目录只有属主可以对其有写权限,其他人只能读。参考/tmp目录,所有人可读,属主可写。

read()

read()系统调用从文件描述符 fd 所指代的打开文件中读取数据。

# include <unistd.h>
ssize_t read(int fd, void *buffer, size_t count);

fd,已打开文件的返回码。
buffer,缓存的地址。
count,一次最多能读取的字节数。
成功返回实际读取的字节数,如果遇到文件结束符(EOF)返回0。
失败返回-1并设置errno。

count,一次最多能读取的字节数。
成功返回实际读取的字节数,如果遇到文件结束符(EOF)返回0。
失败返回-1并设置errno。

  read的文件结尾要有 \n

栗子:
test.txt

cdq\n
#include <iostream>
#include<fcntl.h>
#include<sys/stat.h>
#include<unistd.h>

using namespace std;

int main()
{
    int fd;
    ssize_t num;
    char ttt[]="abc\n";

    cout << "Hello World!" << endl;
    cout <<ttt;

    fd=open("/opt/test.txt",O_RDWR);

    if(fd!=-1)
    {
        num=read(fd,ttt,3);
    }

    if(num!=-1)
    {
        cout<<ttt;
    }

    system("pause");
    return 0;
}

write()

write()系统调用将数据写入一个已打开的文件中。

# include <unistd.h>
ssize_t write(int fd, void *buffer, size_t count);

fd,已打开文件的返回码。
buffer,缓存的地址。
count,要写入的字节数。
成功返回实际写入的字节数,可能会出现部分写的情况(磁盘已满或者进程资源对文件大小有限制)。
失败返回-1,并设置错误码。

close()

close()系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当一进程终止时,将自动关闭其已打开的所有文件描述符。

# include <unistd.h>
int close(int fd);

fd,已打开文件的返回码。
成功返回0。
失败返回-1并设置错误码。

有效性

检查文件有效性有两办法:
1.open()
根据open()返回码判断。
2.access()
有效返回0,无效返回非0.

lseek()

文件偏移量是指执行下一个read()或write()操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为0。

# include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

fd,已打开文件的返回码。
offset,指定了一个以字节为单位的数值。
whence,表明应参照哪个基点来解释offset 参数.
成功返回偏移量,
失败返回-1并设置错误码。

whence 含义
SEEK_SET 0 相对于文件头部开始的offset个字节
SEEK_CUR 1 相对于当前文件偏移量起始的offset个字节
SEEK_END 2 相对于文件尾部起始的offset个字节
lseek(fd, 0, SEEK_SET)        //文件头
lseek(fd, 0, SEEK_END)        //文件尾
lseek(fd, -1, SEEK_END)        //文件尾的前1个字节
lseek(fd, -10, SEEK_CUR)    //文件当前偏移的前10个字节
lseek(fd, 10000, SEEK_END)    //文件尾的后10000个字节
  文件空洞
文件偏移量跨越了文件结尾,如 lseek(fd, 1000, SEEK_END) ,之后再进行I/O操作,read()调用会返回0,write()调用会写入数据,文件结尾到新写入数据之间的这段空间被称为文件空洞。文件空洞不占用任何磁盘空间(大多数情况下),当向文件空洞写入数据时,系统才会为其分配空间。
  原子操作和竞争条件
原子操作:内核保证了某些系统调用会一次性执行,不会被其他进程或线程所中断,原子操作的作用是为了避免竞争条件。 竞争条件:当两个进程同时对同一个资源进行修改操作时,会产生竞争条件。

总结

为了对普通文件执行I/O操作,首先必须调用open()以获得一个文件描述符。随之使用read()和write()执行文件的I/O操作,然后应使用close()释放文件描述符及相关资源。这些系统调用可对所有类型的文件执行I/O 操作。

所有类型的文件和设备驱动都实现了相同的I/O接口,这保证了I/O操作的通用性,同时也意味着在无需针对特定文件类型编写代码的情况下,程序通常就能操作所有类型的文件。

对于已打开的每个文件,内核都维护有一个文件偏移量,这决定了下一次读或写操作的起始位置。读和写操作会隐式修改文件偏移量。使用lseek()函数可以显式地将文件偏移量置为文件中或文件结尾后的任一位置。在文件原结尾处之后的某一位置写入数据将导致文件空洞。从文件空洞处读取文件将返回全0 字节。

对于未纳入标准I/O 模型的所有设备和文件操作而言,ioctl()系统调用是个“百宝箱”。

本章介绍了原子操作的概念,这对于一些系统调用的正确操作至关重要。特别是,指定O_EXCL 标志调用open(),这确保了调用者就是文件的创建者。而指定O_APPEND 标志来调用open(),还确保了多个进程在对同一文件追加数据时不会覆盖彼此的输出。

系统调用fcntl()可以执行许多文件控制操作,其中包括:修改打开文件的状态标志、复制文件描述符。

使用dup()和dup2()系统调用也能实现文件描述符的复制功能。

本章接着研究了文件描述符、打开文件句柄和文件i-node 之间的关系,并特别指出这3个对象各自包含的不同信息。文件描述符及其副本指向同一个打开文件句柄,所以也将共享打开文件的状态标志和文件偏移量。

之后描述的诸多系统调用,是对常规 read()和 write()系统调用的功能扩展。pread()和pwrite()系统调用可在文件的指定位置处执行I/O 功能,且不会修改文件偏移量。readv()和writev()系统调用实现了分散输入和集中输出的功能。preadv()和pwritev()系统调用则集上述两对系统调用的功能于一身。

使用truncate() 和ftruncate()系统调用,既可以丢弃多余的字节以缩小文件大小,又能使用填充为0 的文件空洞来增加文件大小。

本章还简单介绍了非阻塞I/O 的概念,后续章节中还将继续讨论。

LFS 规范定义了一套扩展功能,允许在32 位系统中运行的进程来操作无法以32 位表示的大文件。

运用虚拟目录/dev/fd 中的编号文件,进程就可以通过文件描述符编号来访问自己打开的文件,这在shell 命令中尤其有用。

mkstemp()和tmpfile()函数允许应用程序去创建临时文件。

信号 Signal

信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间。
一个(具有合适权限的)进程能够向另一进程发送信号。信号的这一用法可作为一种同步技术,甚至是进程间通信(IPC)的原始形式。进程也可以向自身发送信号。

引发信号的原因
1.键盘事件 ctrl +c ctrl +
2.非法内存 如果内存管理出错,系统就会发送一个信号进行处理
3.硬件故障 同样的,硬件出现故障系统也会产生一个信号
4.环境切换 比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统

查看信号:

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

常用信号释义:

名称 信号值 描述 SUSv3 默认值
SIGABRT 6 中止进程 core
SIGALRM 14 实时定时器过期 term
SIGBUS 7 (SAMP=10) 内存访问错误 core
SIGCHLD 17(SA=20, MP=18) 终止或者停止子进程 ignore
SIGCONT 18 (SA=19, M=25, P=26) 若停止则继续 cont
SIGEMT undef (SAMP=7) 硬件错误 - term
SIGFPE 8 算术异常 core
SIGHUP 1 挂起 term
SIGILL 4 非法指令 core
SIGINT 2 终端中断 term
SIGIO / 29(SA=23, MP=22) I/O 时可能产生 term
SIGKILL 9 必杀(确保杀死) term
SIGPIPE 13 管道断开 term
SIGPROF 27 (M=29, P=21) 性能分析定时器过期 term
SIGPWR 30(SA=29, MP=19) 电量行将耗尽 - term
SIGQUIT 3 终端退出 core
SIGSEGV 11 无效的内存引用 core
SIGSTKFLT 16 (SAM=undef, P=36) 协处理器栈错误 - term
SIGSTOP 19(SA=17, M=23, P=24) 确保停止 stop
SIGSYS 31 (SAMP=12) 无效的系统调用 core
SIGTERM 15 终止进程 -
SIGTRAP 5 跟踪/断点陷阱 core
SIGTSTP 20 (SA=18, M=24, P=25) 终端停止 stop
SIGTTIN 21 (M=26, P=27) BG1从终端读取 stop
SIGTTOU 22 (M=27, P=28) BG 向终端写 stop
SIGURG 23 (SA=16, M=21, P=29) 套接字上的紧急数据 ignore
SIGUSR1 10 (SA=30, MP=16) 用户自定义信号1 term
SIGUSR2 12 (SA=31, MP=17) 用户自定义信号2 term
SIGVTALRM 26 (M=28, P=20) 虚拟定时器过期 term
SIGWINCH 28 (M=20, P=23) 终端窗口尺寸发生变化 - ignore
SIGXCPU 24 (M=30, P=33) 突破对CPU 时间的限制 core
SIGXFSZ 25 (M=31, P=34) 突破对文件大小的限制 core

进程收到信号的三种处理方式

默认:如果是系统默认的话,那就会终止这个进程
忽略 :信号来了我们不处理,装作没看到 SIGKILL SIGSTOP 不能忽略
捕获并处理 :当信号来了,执行我们自己写的代码(捕获信号这个动作是需要我们完成的) SIGKILL SIGSTOP 不能捕获

不可靠信号和可靠信号
Linux的信号继承自早期的Unix信号,Unix信号的缺陷
1.信号处理函数执行完毕,信号恢复成默认处理方式(Linux已经改进)
2.会出现信号丢失,信号不排队
1-31 都是不可靠的,会出现信号丢失现象
34-64重新设计的一套信号集合,不会出现信号丢失,支持排队,信号处理函数执行完毕,不会恢复成缺省处理方式
实时信号 : 就是可靠信号
非实时信号:不可靠信号

信号处理函数

UNIX 系统提供了两种方法来改变信号处置:signal()和sigaction()。
signal()系统调用,是设置信号处置的原始 API,所提供的接口比sigaction()简单。
但sigaction()不属于 POSIX 标准,在各类 UNIX 平台上的实现不尽相同,因此其用途受到了一定的限制。而 POSIX 标准定义的信号处理接口是 sigaction 函数。故此,sigaction()是建立信号处理器的首选API(强力推荐)。
signal

#include <signal.h>
void (*signal(int sig,void (*handler)(int))) (int);

sigaction

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum:要操作的信号。
act:要设置的对信号的新处理方式。
oldact:原来对信号的处理方式。
返回值:0 表示成功,-1 表示有错误发生。

struct sigaction 类型用来描述对信号的处理,定义如下:

struct sigaction
{
void     (*sa_handler)(int);
sigset_t  sa_mask;
int       sa_flags;
void     (*sa_restorer)(void);
};

sa_handler是信号处理函数,
sa_mask 成员用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,它自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再度发生。
sa_flags 成员用于指定信号处理的行为,它可以是一下值的“按位或”组合。
re_restorer 成员则是一个已经废弃的数据域,不要使用。

sa_flags可选行为:
◆ SA_RESTART:使被信号打断的系统调用自动重新发起。
◆ SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
◆ SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
◆ SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
◆ SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
◆ SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。

sigaction栗子:

main.c

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

static void sig_usr(int signum)
{
    if(signum == SIGUSR1)
    {
        printf("SIGUSR1 received\n");
    }
    else if(signum == SIGUSR2)
    {
        printf("SIGUSR2 received\n");
    }
    else
    {
        printf("signal %d received\n", signum);
    }
}

int main(void)
{
    char buf[512];
    int  n;
    struct sigaction sa_usr;
    sa_usr.sa_flags = 0;
    sa_usr.sa_handler = sig_usr;   //信号处理函数

    sigaction(SIGUSR1, &sa_usr, NULL);
    sigaction(SIGUSR2, &sa_usr, NULL);

    printf("My PID is %d\n", getpid());

    while(1)
    {
        if((n = read(STDIN_FILENO, buf, 511)) == -1)
        {
            if(errno == EINTR)
            {
                printf("read is interrupted by signal\n");
            }
        }
        else
        {
            buf[n] = '\0';
            printf("%d bytes read: %s\n", n, buf);
        }
    }

    return 0;
}
gcc main.c
./a.out

在这个例程中使用 sigaction 函数为 SIGUSR1 和 SIGUSR2 信号注册了处理函数,然后从标准输入读入字符。程序运行后首先输出自己的 PID,如:My PID is 5904
此时输入内容会有得到输出。
这时启用另一个终端向进程发送 SIGUSR1 或 SIGUSR2 信号,用类似如下的命令:kill -USR1 5904
则程序将继续输出如下内容:

SIGUSR1 received
read is interrupted by signal

这说明用 sigaction 注册信号处理函数时,不会自动重新发起被信号打断的系统调用。如果需要自动重新发起,则要设置 SA_RESTART 标志,比如在上述例程中可以进行类似一下的设置:

sa_usr.sa_flags = SA_RESTART;

发送信号

与shell 的kill 命令相类似,一个进程能够使用kill()系统调用向另一进程发送信号。(之所以选择kill 作为术语,是因为早期UNIX 实现中大多数信号的默认行为是终止进程。)
发送信号

#include <signal.h>
int kill(pid_t pid,int sig);
pid > 0 :发送给pid进程
pid = 0 :调用者所在进程组的任一进程
pid = -1:有权发送的任何一个进程,除了1
pid < -1:|pid|进程组所有的进程,广播

给自己发信号

#include <signal.h>
int raise(int signum);
kill(getpid() ,signum);

给进程组所有成员发信号

#include <signal.h>
int killpg(pid_t pgrp,int signum);

如果指定 pgrp 的值为0,那么会向调用者所属进程组的所有进程发送此信号.
等待信号
调用pause()将暂停进程的执行,直至信号处理器函数中断该调用为止(或者直至一个未处理信号终止进程为止)。

#include<unistd.h>
int pause(void);

SIGALRM
alarm()函数的主要功能是设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。
当sec规定的时间到了,发送SIGALRM信号给本进程,如果sec是0,表示清除信号.
要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。需要注意的是,经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一些时间。

int alarm(int sec);

闹钟栗子:
5秒内没有操作,则输出“超时”,5秒内输入了字符,则清空闹钟,然后进入死循环,按下ctrl+c退出。

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

void handler(int s); //信号到来,则执行这个函数,输出超时

int main()
{
  char buf[100]={};

  signal(SIGALRM,handler); //定义一个信号函数,当SIGALRM信号发过来时,执行handler函数

  alarm(5);  //设置五秒的时钟,五秒内如果没有执行输入操作就会发送信号。注意,这条执行后会立刻执行下一条,而不是等5秒再执行下一条。
  printf("输入名字");   
  scanf("%s",buf);
  alarm(0);  //如果五秒内执行了操作,那就清空闹钟

  printf("名字为:%s\n",buf);

  for(; ;) //验证闹钟时间已经清空
  {
    fflush(stdout); 
    printf(".");
    sleep(1);
  }
}

void handler(int s) 
{
  printf("超时\n");
  exit(1);
}

5秒内输入字符:

输入名字2
名字为:2
........

5秒内没输入字符:

输入名字超时

定时器

间隔型定时器:每隔一段时间触发一次,setitimer()和alarm()。
休眠:即延时,sleep()。
注意:Linux中只允许一个进程中有一个间隔型定时器。
在信号中已经介绍了alarm(),就此略过。这里介绍 setitimer() 和 sleep()。

setitimer()

原型:

#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

which,可以创建 3 种不同类型的定时器。
struct itimerval,定时器模式设定。
old_value,上一次定时器的值,一般置为NULL即可

which:
ITIMER_REAL:创建以真实时间倒计时的定时器。到期时会产生SIGALARM 信号并发送给进程。
ITIMER_VIRTUAL:创建以进程虚拟时间(用户模式下的CPU 时间)倒计时的定时器。到期时会产生信号SIGVTALRM。
ITIMER_PROF:创建一个profiling 定时器,以进程时间(用户态与内核态CPU 时间的总和)倒计时。到期时,则会产生 SIGPROF 信号。

struct itimerval

struct itimerval {
               struct timeval it_interval; /* next value */
               struct timeval it_value; /* current value */
           };
struct timeval {
                long tv_sec; /* seconds */
                long tv_usec; /* microseconds */
            };

其中it_value表示设置定时器后间隔多久开始执行定时任务,而it_interval表示两次定时任务之间的时间间隔。

setitimer栗子:

#include <stdio.h>    // for printf()  
#include <signal.h> 
#include <sys/time.h>

#include <errno.h>

void sigFunc()
{
   static int iCnt = 0;
   printf("The %d Times: Hello world\n", iCnt++);
}

int main(void)
{
   struct itimerval tv, otv;
   signal(SIGALRM, sigFunc);
   //how long to run the first time
   tv.it_value.tv_sec = 3;
   tv.it_value.tv_usec = 0;
   //after the first time, how long to run next time
   tv.it_interval.tv_sec = 1;
   tv.it_interval.tv_usec = 0;

   if (setitimer(ITIMER_REAL, &tv, &otv) != 0)
    printf("setitimer err %d\n", errno);

   while(1)
   {
      sleep(1);
      printf("otv: %ld, %ld, %ld, %ld\n", otv.it_value.tv_sec, otv.it_value.tv_usec, otv.it_interval.tv_sec, otv.it_interval.tv_sec);
   }
}

编译会出现警告,可以忽略。

otv: 0, 0, 0, 0
otv: 0, 0, 0, 0
The 0 Times: Hello world
otv: 0, 0, 0, 0
The 1 Times: Hello world
otv: 0, 0, 0, 0
The 2 Times: Hello world
otv: 0, 0, 0, 0
The 3 Times: Hello world
otv: 0, 0, 0, 0

sleep()

windows下 sleep(1000) 代表延迟1秒,因为sleep的参数为毫秒,
而在Linux下 sleep的参数为秒,所以延迟1秒为 sleep(1)。

进程

在诸多应用中,创建多个进程是任务分解时行之有效的方法。
例如,某一网络服务器进程可在侦听客户端请求的同时,为处理每一请求而创建一新的子进程,与此同时,服务器进程会继续侦听更多的客户端连接请求。以此类手法分解任务,通常会简化应用程序的设计,同时提高了系统的并发性。(即,可同时处理更多的任务或请求。)

创建进程

系统调用 fork()创建一新进程(child),几近于对调用进程(parent)的翻版。
每当调用一次fork函数时,会返回两次pid。
一次是在调用进程中(父进程)返回一次,返回值是新派生的进程的进程ID。
一次是在子进程中返回,返回值是0,代表当前进程为子进程。如果返回-1,那么则代表在创建子进程的过程中出现了错误。

#include <unistd.h>
pid_t fork(void);

返回值:
父进程:返回值大于0,子进程的pid
子进程:返回值等于0

栗子:

#include <stdio.h>
#include <unistd.h>   
int main(){    
  printf("parent pid:%d\n",getpid());    
  int a = 100;    
  pid_t pid = fork();    
  if(pid < 0){    
    return -1;    
  }else if(pid == 0){    
    a = 20;    
    printf("child !! pid:%d----a:%d--%p\n",getpid(),a ,&a);    

  }else{    
    sleep(1);    
    printf("parent !! pid:%d----a:%d--%p\n",getpid(), a, &a);    
  }    
  printf("nihaoa %d\n",a);    
  return 0;    
}  
parent pid:20372
child !! pid:20373----a:20--0x7ffc05bf8da0
nihaoa 20
parent !! pid:20372----a:100--0x7ffc05bf8da0
nihaoa 100

先返回了子进程的pid,之后再返回了父进程的pid。
fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。
fork()相当于创建了一个新的子进程,但是拷贝的是fork()函数之后的所有数据,之前的并不会拷贝。在代码之上就可以看到parent pid:20372只打印了一次
调用 fork()之后,系统将率先“垂青”于哪个进程(即调度其使用CPU),是无法确定的,意识到这一点极为重要。在设计拙劣的程序中,这种不确定性可能会导致所谓“竞争条件(racecondition)”的错误

fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。

进程的终止

通常,进程有两种终止方式。其一为异常(abnormal)终止,如20.1 节所述,由对一信号的接收而引发,该信号的默认动作为终止当前进程,可能产核心转储(core dump)。此外,进程可使用_exit()系统调用正常(normally)终止。

_exit

#include<unistd.h>
void _exit(int status);

status:0代表成功,非0没有确切定义。

  注意
虽然可将 0~255 之间的任意值赋给_exit()的status 参数,并传递给父进程,不过如取值大于128 将在shell 脚本中引发混乱。原因在于,当以信号(signal)终止一命令时,shell 会将变量$?置为 128 与信号值之和,以表征这一事实。如果这与进程调用_exit()时所使用的相同status 值混杂起来,将令shell 无法区分。

exit
程序一般不会直接调用_exit(),而是调用库函数 exit(),它会在调用_exit()前执行各种动作。

#include<stdlib.h>
void exit(int status);

exit()会执行的动作如下。
调用退出处理程序(通过 atexit()和 on_exit()注册的函数),其执行顺序与注册顺序相反。
刷新 stdio 流缓冲区。
使用由 status 提供的值执行_exit()系统调用。

程序的另一种终止方法是从 main()函数中返回(return),或者或明或暗地一直执行到main()函数的结尾处。执行return n 等同于执行对exit(n)的调用,因为调用 main()的运行时函数会将 main()的返回值作为 exit()的参数。

监控子进程

wait()

系统调用wait()等待调用进程的任一子进程终止,同时在参数status 所指向的缓冲区中返回该子进程的终止状态。

#include<sys/wait.h>
pid_t wait(int *status);

系统调用 wait()执行如下动作。
1.如果调用进程并无之前未被等待的子进程终止,调用将一直阻塞,直至某个子进程终止。如果调用时已有子进程终止,wait()则立即返回。
2.如果 status 非空,那么关于子进程如何终止的信息则会通过status 指向的整型变量返回。
3.内核将会为父进程下所有子进程的运行总量追加进程CPU 时间(10.7 节)以及资源使用数据。
4.将终止子进程的 ID 作为wait()的结果返回。

出错时,wait()返回-1。可能的错误原因之一是调用进程并无之前未被等待的子进程,此时会将errno 置为ECHILD。

waitpid()

系统调用wait()存在诸多限制,而设计waitpid()则意在突破这些限制。
1.如果父进程已经创建了多个子进程,使用 wait()将无法等待某个特定子进程的完成,只能按顺序等待下一个子进程的终止。
2.如果没有子进程退出,wait()总是保持阻塞。有时候会希望执行非阻塞的等待:是否有子进程退出,立判可知。
3.使用 wait()只能发现那些已经终止的子进程。对于子进程因某个信号(如SIGSTOP 或SIG TTIN)而停止,或是已停止子进程收到SIGCONT 信号后恢复执行的情况就无能为力了。

#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);

如果 pid 大于0,表示等待进程ID 为pid 的子进程。
如果 pid 等于0,则等待与调用进程(父进程)同一个进程组(process group)的所有子进程。
如果 pid 小于-1,则会等待进程组标识符与pid 绝对值相等的所有子进程。
如果 pid 等于-1,则等待任意子进程。wait(&status)的调用与waitpid(-1, &status, 0)等价。

参数 options:
WUNTRACED:除了返回终止子进程的信息外,还返回因信号而停止的子进程信息。
WCONTINUED:返回那些因收到SIGCONT 信号而恢复执行的已停止子进程的状态信息。
WNOHANG:如果参数pid 所指定的子进程并未发生状态改变,则立即返回,而不会阻塞,亦即poll(轮询)。在这种情况下,waitpid()返回0。如果调用进程并无与pid 匹配的子进程,则waitpid()报错,将错误号置为ECHILD。

参数 status:
子进程的结束状态返回后存于 status,底下有几个宏可判别结束情况:
WIFEXITED(status)如果若为正常结束子进程返回的状态,则为真;对于这种情况可执行WEXITSTATUS(status),取子进程传给exit或_eixt的低8位。
WEXITSTATUS(status)取得子进程 exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
WIFSIGNALED(status)若为异常结束子进程返回的状态,则为真;对于这种情况可执行WTERMSIG(status),取使子进程结束的信号编号。
WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真;对于这种情况可执行WSTOPSIG(status),取使子进程暂停的信号编号。
WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用 WIFSTOPPED 来判断后才使用此宏

栗子:
main.c

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
int main( void )
{
    pid_t childpid;
    int status;
    childpid = fork();
    if ( childpid < 0 )
    {
        perror( "fork()" );
        exit( EXIT_FAILURE );
    }
    else if ( childpid == 0 )
    {
        puts( "In child process" );
        sleep( 3 );//让子进程睡眠3秒,看看父进程的行为
        printf("\tchild pid = %d\n", getpid());
        printf("\tchild ppid = %d\n", getppid());
        exit(EXIT_SUCCESS);
    }
    else
    {
        waitpid( childpid, &status, 0 );//等待子进程退出
        puts( "in parent" );
        printf( "\tparent pid = %d\n", getpid() );
        printf( "\tparent ppid = %d\n", getppid() );
        printf( "\tchild process exited with status %d \n", status );
    }
    exit(EXIT_SUCCESS);
}
gcc main.c
./a.out
In child process
child pid = 4469
child ppid = 4468
in parent
parent pid = 4468
parent ppid = 4379
child process exited with status 0

如果将上面“waitpid( childpid, &status, 0 );”行注释掉,程序执行效果如下:

[root@localhost src]# ./a.out
n child process
in parent
parent pid = 4481
parent ppid = 4379
child process exited with status1
[root@localhost src]# child pid = 4482
child ppid = 1

SIGCHLD

无论一个子进程于何时终止,系统都会向其父进程发送SIGCHLD 信号。

https://blog.csdn.net/qq_33883085/article/details/89325396

总结

使用wait()和waitpid()(以及其他相关函数),父进程可以得到其终止或停止子进程的状态。该状态表明子进程是正常终止(带有表示成功或失败的退出状态),还是异常中止,因收到某个信号而停止,还是因收到SIGCONT 信号而恢复执行。

如果子进程的父进程终止,那么子进程将变为孤儿进程,并为进程 ID 为 1 的init 进程接管。

子进程终止后会变为僵尸进程,仅当其父进程调用 wait()(或类似函数)获取子进程退出状态时,才能将其从系统中删除。在设计长时间运行的程序,诸如shell 程序以及守护进程(daemon)时,应总是捕获其所创建子进程的状态,因为系统无法杀死僵尸进程,而未处理的僵尸进程最终将塞满内核进程表。

捕获终止子进程的一般方法是为信号SIGCHLD 设置信号处理程序。当子进程终止时(也可选择子进程因信号而停止时),其父进程会收到SIGCHLD 信号。还有另一种移植性稍差的处理方法,进程可选择将对SIGCHLD 信号的处置置为忽略(SIG_IGN),这时将立即丢弃终止子进程的状态(因此其父进程从此也无法获取到这些信息),子进程也不会成为僵尸进程。

system()

程序可通过调用system()函数来执行任意的 shell 命令。

#include <stdlib.h>
int system(const char *command);

栗子:
sys.c

 #include <stdio.h>
 #include <stdlib.h>
 int main(void)
 {
     system("ls -l ~");
     return 0;
 }

``bash
./a.out

总用量 44
drwxr-xr-x 2 pi pi 4096 12月 8 18:30 cdq
drwxr-xr-x 2 pi pi 4096 12月 7 23:42 Desktop
drwxr-xr-x 4 pi pi 4096 3月 5 2019 Documents
drwxr-xr-x 5 pi pi 4096 5月 4 2019 Downloads
drwxr-xr-x 2 pi pi 4096 6月 27 2018 Music
drwxr-xr-x 11 pi pi 4096 12月 8 09:44 oldFiles
drwxr-xr-x 2 pi pi 4096 6月 27 2018 Pictures
drwxr-xr-x 2 pi pi 4096 6月 27 2018 Public
drwxr-xr-x 2 pi pi 4096 6月 27 2018 Templates
drwxr-xr-x 5 root root 4096 12月 29 2018 venv
drwxr-xr-x 2 pi pi 4096 1月 18 2019 Videos


## 进程状态
进程状态一般有:就绪态,阻塞态,运行态。
在Linux下:R运行状态,S睡眠状态,D磁盘休眠状态,T停止状态,X死亡状态
这些当我们使用指令`ps -aux`就可以看到

## 僵尸进程和孤儿进程
### 僵尸进程
在进程状态中有两个比较特殊的存在。僵尸和孤儿
僵尸进程是进程退出后,但是资源没有释放,处于僵死状态的进程。

产生原因:子进程先于父进程退出,操作系统检测到进程的退出,通知父进程,但是父进程这时候正在执行其他操作,没有关注这个通知,这时候操作系统为了保护子进程,不会释放子进程资源,因为子进程的PCB中包含有退出原因。这时候因为既没有运行也没有退出,因此处于僵死状态,成为僵尸进程。
```c
#include <stdio.h>    
#include <stdlib.h>    
#include <unistd.h>    
#include <errno.h>                                                                                                         
int main()    
{    
  pid_t  pid;    
  //循环创建子进程    
  while(1)    
  {    
    pid = fork();    
    if (pid < 0)    
    {    
      perror("fork error:");    
      exit(1);    
    }    
    else if (pid == 0)    
    {    
      printf("I am a child process.\nI am exiting.\n");    
      //子进程退出,成为僵尸进程    
      exit(0);    
    }    
    else    
    {    
      //父进程休眠20s继续创建子进程    
      sleep(20);    
      continue;
    }       
  }      
  return 0;                            
}

执行这上面这个程序,子进程中途退出了。

I am a child process.
I am exiting.

查看进程

ps -aux|grep a.out 
paralle+ 29711  0.0  0.0   4376   708 pts/0    S+   14:15   0:00 ./a.out
paralle+ 29712  0.0  0.0      0     0 pts/0    Z+   14:15   0:00 [a.out] <defunct>
paralle+ 29760  0.0  0.1  21536  1024 pts/1    S+   14:16   0:00 grep --color=auto a.out

z+这个标志就是僵尸进程的标志。
那么怎么避免僵尸进程的产生?
我们一般处理就是关闭父进程,这样僵尸子进程也随之消失了。

孤儿进程

孤儿进程与僵尸进程在理解上可以认为相反。
父进程先于子进程退出,父进程退出后,子进程成为后台进程,并且父进程为1号进程。
守护进程:特殊(脱离了与终端的关联+会话的关联)的孤儿进程

优先级

设置优先级,可以使用指令ps -elf先查看进程,可以看到 PRI 和 NI这两个数值:
PRI:优先级
NI:nice值

PRI是无法直接调整的,但是可以通过调整nice值(-20~19)来调整优先级的大小.
指令操作为renice -n size -p pid
运行时操作为nice -n size ./main(可执行文件)

Linux下指令top指令可以查看进程的优先级
进入top后按“r”–>输入进程PID–>输入nice值

进程的创建速度

进程的创建速度

线程

启动程序时,产生的进程只有单条线程,称之为初始(initial)或主(main)线程。

创建线程

#include<pthread.h>
int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg);

参数:
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
最后一个参数是运行函数的参数。

  编译链接参数
-lpthread

终止线程

可以如下方式终止线程的运行。
1.线程 start 函数执行return 语句并返回指定值。
2.线程调用 pthread_exit()(详见后述)。
3.调用 pthread_cancel()取消线程(在32.1 节讨论)。
4.任意线程调用了exit(),或者主线程执行了return 语句(在main()函数中),都会导致进程中的所有线程立即终止。

pthread_exit()函数将终止调用线程,且其返回值可由另一线程通过调用pthread_join()来获取。

#include<pthread.h>
void pthread_exit(void *retval);

线程ID

进程内部的每个线程都有一个唯一标识,称为线程ID。线程ID 会返回给pthread_create()的调用者,一个线程可以通过pthread_self()来获取自己的线程ID。

#include<pthread.h>
pthread_t pthread_self(void);

函数 pthread_equal()可检查两个线程的ID 是否相同。

#include<pthread.h>
int pthread_equal(pthread_t t1,pthread_t t2);

成功返回非0值,否则返回0.

回收线程

函数pthread_join用来等待一个线程的结束,线程间同步的操作。
线程的创建类似于 new ,申请了一份内存,如果不进行回收,则会发生类似于使用 new 而没delete的内存泄漏现象。
pthread_join用来等待并回收一个线程。

#include<pthread.h>
int pthread_join(pthread_t thread,void **retval);

thread: 线程标识符,即线程ID,标识唯一线程。
retval: 用户定义的指针,用来存储被等待线程的返回值。

栗子:

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

void printids(const char *s)
{
    pid_t pid;
    pthread_t tid;
    pid = getpid();
    tid = pthread_self();
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int) pid,
            (unsigned int) tid, (unsigned int) tid);
}

void *thr_fn(void *arg)
{
    printids("new thread: ");
    return NULL;
}

int main(void)
{
    int err;
    pthread_t ntid;
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0)
        printf("can't create thread: %s\n", strerror(err));
    printids("main thread:");
    pthread_join(ntid,NULL);
    return EXIT_SUCCESS;
}
gcc pthread.c -pthread
./a.out
new thread:  pid 2680 tid 1993790576 (0x76d6d470)
main thread: pid 2680 tid 1995669424 (0x76f37fb0)

总结

在多线程程序中,多个线程并发执行同一程序。所有线程共享相同的全局和堆变量,但每个线程都配有用来存放局部变量的私有栈。同一进程中的线程还共享一干其他属性,包括进程ID、打开的文件描述符、信号处置、当前工作目录以及资源限制。
线程与进程间的关键区别在于,线程比进程更易于共享信息,这也是许多应用程序舍进程而取线程的主要原因。对于某些操作来说(例如,创建线程比创建进程快),线程还可以提供更好的性能。但是,在程序设计的进程/线程之争中,这往往不会是决定性因素。
可使用 pthread_create()来创建线程。每个线程随后可调用pthread_exit()独立退出。(如有任一线程调用了exit(),那么所有线程将立即终止。)除非将线程标记为分离状态(例如通过调用pthread_detached()),其他线程要连接该线程,则必须使用pthread_join(),由其返回遭连接线程的退出状态。

线程同步

互斥量

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正由其他线程修改的变量。术语临界区(critical section)是指访问某一共享资源的代码片段,并且这段代码的执行应为原子(atomic)操作,亦即,同时访问同一共享资源的其他线程不应中断该片段的执行。

静态分配互斥量

互斥量是属于pthread_mutex_t 类型的变量。在使用之前必须对其初始化。
POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁:

#include <pthread.h>
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 

初始化之后,互斥量处于未锁定状态。函数pthread_mutex_lock()可以锁定某一互斥量,而函数pthread_mutex_unlock()则可以将一个互斥量解锁。

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

栗子:
未加锁打印数据
main.c

#include <stddef.h>  
#include <stdio.h>  
#include <unistd.h>  
#include <pthread.h>  
#include <unistd.h>

void* process(void * arg)  
{   
  fprintf(stderr, "Starting process %s\n", (char *) arg);  
  for(int i = 0; i < 100; i++)
  {   
    fprintf(stdout, (char *) arg, 1);  
  }  

  return NULL;  
}  


int hello(){  
    printf("hello");  
    return 1;  
}  

int main()  
{  
  int retcode;  
  pthread_t th_a, th_b;  
  void * retval;  

  retcode = pthread_create(&th_a, NULL, process, "a");  
  if (retcode != 0) fprintf(stderr, "create a failed %d\n", retcode);  

  retcode = pthread_create(&th_b, NULL, process, "b");  
  if (retcode != 0) fprintf(stderr, "create b failed %d\n", retcode);  

  retcode = pthread_join(th_a, &retval);  
  if (retcode != 0) fprintf(stderr, "join a failed %d\n", retcode);  

  retcode = pthread_join(th_b, &retval);  
  if (retcode != 0) fprintf(stderr, "join b failed %d\n", retcode);  

  return 0;  
}  

此时a和b的打印每次输出都是不同的乱序的。

gcc main.c -pthread
./a.out
Starting process a
Starting process b
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaababaabaababbabababaababababababababababababababababababababababababababaabababababababababababababababababababababaababababababbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbpi@raspberrypi:~/cdq $

加锁打印数据
main.c

#include <stddef.h>  
#include <stdio.h>  
#include <unistd.h>  
#include "pthread.h"  
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;  
void * process(void * arg)  
{  
  int i;  
  fprintf(stderr, "Starting process %s\n", (char *) arg);  
  pthread_mutex_lock(&mymutex);  
  for (i = 0; i < 100; i++) {  
      fprintf(stdout, (char *) arg, 1);  
  }  
  pthread_mutex_unlock(&mymutex);   
  return NULL;  
}  
int hello(){  
        printf("hello");  
        return 1;  
}  
int main()  
{  
  int retcode;  
  pthread_t th_a, th_b;  
  void * retval;  

  retcode = pthread_create(&th_a, NULL, process, "a");  
  if (retcode != 0) fprintf(stderr, "create a failed %d\n", retcode);  

  retcode = pthread_create(&th_b, NULL, process, "b");  
  if (retcode != 0) fprintf(stderr, "create b failed %d\n", retcode);  

  retcode = pthread_join(th_a, &retval);  
  if (retcode != 0) fprintf(stderr, "join a failed %d\n", retcode);  

  retcode = pthread_join(th_b, &retval);  
  if (retcode != 0) fprintf(stderr, "join b failed %d\n", retcode);  

  return 0;  
}

无论a,b是什么时候开始,一定是一个结束之后,锁释放后, 第二个才开始

gcc main.c -pthread
./a.out
Starting process b
Starting process a
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaapi@raspberrypi:~/cdq $

动态分配互斥量

创建互斥量
动态方式是采用pthread_mutex_init()函数来初始化互斥锁

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) 

其中mutexattr用于指定互斥锁属性,如果为NULL则使用默认值。

互斥锁属性:
PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。

销毁互斥量

pthread_mutex_destroy ()用于注销一个互斥锁:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex)

销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。

注意:
POSIX 线程锁机制的Linux实现都不是取消点,因此,延迟取消类型的线程不会因收到取消信号而离开加锁等待。值得注意的是,如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,或者设置了异步取消类型,则必须在退出回调函数中解锁。
这个锁机制同时也不是异步信号安全的,也就是说,不应该在信号处理过程中使用互斥锁,否则容易造成死锁。

条件变量

就是等待某一条件的发生,和信号一样。
条件变量使我们可以睡眠等待某种条件出现。
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

静态创建

初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

动态创建

创建

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)

销毁锁

int pthread_cond_destroy(pthread_cond_t *cond)

操作函数

pthread_cond_init(&cond, NULL); /* 动态初始化条件变量 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; /* 静态初始化条件变量 */
pthread_cond_wait(&cond); /* 等待条件变量触发 */
pthread_cond_timedwait(&cond); /* 超时等待条件变量触发 */
pthread_cond_signal(&cond); /* 激活一个等待该条件的线程,单播 */
pthread_cond_broadcast(&cond); /* 激活所有等待该条件的线程,广播 */
pthread_cond_destroy(&cond); /* 销毁条件变量 */

栗子:
下面的例子目的是让thread_2先于thread_1执行:
main.c

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

pthread_mutex_t lock;
pthread_cond_t cond;

void *thread_1(void *data)
{
    pthread_mutex_lock(&lock);
    pthread_cond_wait(&cond, &lock);
    printf("%s\n", __func__);
    pthread_mutex_unlock(&lock);
}

void *thread_2(void *data)
{
    pthread_mutex_lock(&lock);
    printf("%s\n", __func__);
    pthread_mutex_unlock(&lock);

    pthread_cond_signal(&cond);
}

int main(int argc, char const *argv[])
{
    pthread_t pid[2];
    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&cond, NULL); 

    pthread_create(&pid[0], NULL, thread_1, NULL);
    pthread_create(&pid[1], NULL, thread_2, NULL);

    pthread_join(pid[0], NULL);
    pthread_join(pid[1], NULL);

    pthread_mutex_unlock(&lock);
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);

    return 0;
}
gcc main.c -pthread
./a.out
thread_2
thread_1

条件变量始终都会和一个互斥锁配合使用,当pthread_cond_wait()被调用阻塞住线程的时候,lock会被自动释放,当信号来的时候会自动上锁。thread_1获取到互斥锁之后,因为条件变量的存在,thread_1被阻塞住,互斥锁自动释放掉,当条件变量满足条件之后系统会将互斥锁再重新锁上.

单播和广播实例:
main.c

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

pthread_mutex_t lock;
pthread_cond_t cond;

void *thread_1(void *data)
{
    pthread_mutex_lock(&lock);
    pthread_cond_wait(&cond, &lock);
    printf("%s\n", __func__);
    pthread_mutex_unlock(&lock);
}

void *thread_2(void *data)
{
    pthread_mutex_lock(&lock);
    pthread_cond_wait(&cond, &lock);
    printf("%s\n", __func__);
    pthread_mutex_unlock(&lock);
}

int main(int argc, char const *argv[])
{
    int cid = 0;
    pthread_t pid[2];
    pthread_mutex_init(&lock, NULL);
    pthread_cond_init(&cond, NULL);

    pthread_create(&pid[0], NULL, thread_1, NULL);
    pthread_create(&pid[1], NULL, thread_2, NULL);

    while (1) {
        scanf("%d", &cid);
        getchar();

        if (cid == 1) {
            pthread_cond_signal(&cond); /* 单播,向其中一个线程发送信号 */
        } else if (cid == 2) {
            pthread_cond_broadcast(&cond); /* 广播,向所有等待条件变量的线程发送信号 */
        } else if (cid == 3) {
            break;
        }
    }

    pthread_join(pid[0], NULL);
    pthread_join(pid[1], NULL);

    pthread_mutex_unlock(&lock);
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&cond);

    return 0;
}
gcc main.c -pthread
./a.out
1
thread_1
2
thread_2
3
pi@raspberrypi:~/cdq $ ./a.out
2
thread_1
thread_2
1
3

pthread_cond_signal()发送的是单播信号,也就是thread_1和thread_2中会有一个运行,发送几次就会有几个线程接收到信号。
pthread_cond_broadcast()发送的是广播信号,当它执行的时候thread_1和thread_2会一起收到信号,事实上无论有多少个线程在等待cont条件,如果想让它们都运行起来,通过pthread_cond_broadcast()广播即可.

线程安全函数

若函数可同时供多个线程安全调用,则称之为线程安全函数;反之,如果函数不是线程安全的,则不能并发调用。

为便于开发多线程应用程序,除了下表所列函数以外,SUSv3 中的所有函数都需实现线程安全。

线程安全函数

一次性初始化

多线程程序有时有这样的需求:不管创建了多少线程,有些初始化动作只能发生一次。例如,可能需要执行 pthread_mutex_init()对带有特殊属性的互斥量进行初始化,而且必须只能初始化一次。如果由主线程来创建新线程,那么这一点易如反掌:可以在创建依赖于该初始化的线程之前进行初始化。不过,对于库函数而言,这样处理就不可行,因为调用者在初次调用库函数之前可能已经创建了这些线程。故而需要这样的库函数:无论首次为任何线程所调用,都会执行初始化动作。

#include<pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))

利用参数 once_control 的状态, 函数 pthread_once() 可以确保无论有多少线程对pthread_once()调用了多少次,也只会执行一次由init 指向的调用者定义函数。

main.c

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

pthread_once_t once = PTHREAD_ONCE_INIT;

void once_run(void)
{
    cout<<"once_run in thread "<<(unsigned int )pthread_self()<<endl;
}

void * child1(void * arg)
{
    pthread_t tid =pthread_self();
    cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
    pthread_once(&once,once_run);
    cout<<"thread "<<tid<<" return"<<endl;
}


void * child2(void * arg)
{
    pthread_t tid =pthread_self();
    cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
    pthread_once(&once,once_run);
    cout<<"thread "<<tid<<" return"<<endl;
}

int main(void)
{
    pthread_t tid1,tid2;
    cout<<"hello"<<endl;
    pthread_create(&tid1,NULL,child1,NULL);
    pthread_create(&tid2,NULL,child2,NULL);
    sleep(10);
    cout<<"main thread exit"<<endl;
    return 0;
}
g++ main.cpp -pthread
./a.out
hello
thread 3086535584 enter
once_run in thread 3086535584
thread 3086535584 return
thread 3076045728 enter
thread 3076045728 return
main thread exit

线程取消

发起请求

函数pthread_cancel()向由thread 指定的线程发送一个取消请求。

#include<pthread.h>
int pthread_cancel(pthread_t thread);

TODO:)

参考链接:
https://blog.csdn.net/zb1593496558/article/details/80280346
https://blog.csdn.net/skrskr66/article/details/89147940


文章作者: 陈德强
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈德强 !
¥
  目录