Linux系统编程

Linux系统编程

引言

概念

  • Linux系统编程就是基于linux提供的系统调用函数接口来进行编程
  • 系统调用严格意义来讲是系统函数而非真系统调用,比方说read函数,他其实是将真正的系统调用函数sys_read函数封装了一层来提供的,所以read函数只能说是系统函数
  • 对于Linux的C++开发程序员来说,在Linux系统下开发以及懂得使用Linux的系统函数尤为重要

静态库和动态库

静态库

概念

  • 静态库在可执行程序运行前就加入到执行码中,成为执行程序的一部分
  • 在生成可执行程序后,可执行程序的占用空间包括了静态库的大小
  • 静态库适用于对于空间要求较低,对时间要求较高的核心程序

制作静态库及使用

  1. 准备好要制作静态库的程序文件和声明文件.h

  2. 生成目标文件

    1
    2
    gcc -c 程序文件.cpp -o 程序文件.o
    //可以生成多个目标文件,最后将这些全部做到一个静态库中
  3. 生成静态库

    1
    ar rcs lib库名.a 目标文件1.o 目标文件2.o ...
  4. 使用静态库

    1
    2
    3
    4
    5
    6
    7
    8
    //在要使用静态库程序中引用.h文件
    //然后编译程序文件时使用静态库
    //c++
    g++ 运行程序.cpp lib库名.a -o 运行程序


    //c
    gcc 运行程序.cpp lib库名.a -o 运行程序

动态库

概念

  • 动态库(共享库)在执行程序启动时加载到执行程序中,可以被多个执行能够程序共享使用
  • 动态库是不会占用执行程序空间,也就是执行程序中的占用空间里不包含动态库,而可执行程序可调用动态库
  • 动态库适用于对空间要求较高,对时间要求较低的核心程序

制作动态库及使用

  1. 准备好要制作动态库的程序文件和声明文件.h

  2. 生成与位置无关的代码将.c或.cpp生成.o文件

    1
    2
    gcc -c 文件.cpp -o 文件.o -fPIC
    //-fPIC参数将会生成与位置无关代码
  3. 制作动态库

    1
    gcc -shared -o lib库名.so 目标文件.o 目标文件1.o ...
  4. 使用动态库

    1
    2
    3
    4
    5
    6
    7
    8
    //编译可执行程序时,指定所使用的动态库
    //-l:指定库名
    //-L:指定库路径
    #c
    gcc 可执行程序.cpp -o 可执行程序.out -l 库名 -L 库路径

    #c++
    gcc 可执行程序.cpp -o 可执行程序.out -l 库名 -L 库路径
  5. 运行可执行文件.out

    1
    2
    3
    ./test.out
    #报错
    #./test.out: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

使用动态库后的.out程序运行报错解决

  • 原因:

    链接器:工作于链接阶段,工作时需要-l和-L

    动态链接器:工作于程序运行阶段,工作时需要提供动态库所在目录位置(会去默认目录去找)

    原因:因为使用动态库,所使用的是动态链接器,则工作时需要找到动态库,因为他在默认目录未找到,所以导致报错

  • 解决:

    1. 设置临时环境变量

      1
      2
      3
      4
      export LD_LIBRARY_PATH=库路径

      ./test.out
      #运行成功
    2. **编辑终端配置文件(永久)**:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #去到你对应的终端,bash就是.bashrc,zsh就是.zshrc
      vim .zshrc

      #添加,库路径建议使用绝对路径
      export LD_LIBRARY_PATH=库路径
      #保存

      #生效配置文件
      source .zshrc

      ./test.out
      #运行成功

open/close函数

函数原型

  • int open(const char *pathname,int flags)用来打开一个文件和创建一个文件(如果没有该文件就创建)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <fcntl.h>

    int open(const char *pathname,int flags);

    //参数
    pathname:打开文件的路径名;

    flags:操作标志参数,多个可以使用|包括;
    //flags参数选择:
    O_RDONLY:只读;
    O_WRONLY:只写;
    O_RDWR:读和写;
    O_APPEND:追加;
    O_CREAT:创建;
    O_EXCL:文件是否存在;
    O_TRUNC:截断;
    O_NONBLOCK:非阻塞;
    //返回值
    成功:返回新的文件描述符;

    失败:-1,errno;
  • int open(const char *pathname,int flags,mode_t mode)用来打开一个文件和创建一个文件(如果没有该文件就创建)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include <fcntl.h>

    int open(const char *pathname,int flags,mode_t mode)

    //参数
    pathname:打开文件的路径名;

    flags:操作标志参数,多个可以使用|包括;
    //flags参数选择:
    O_RDONLY:只读;
    O_WRONLY:只写;
    O_RDWR:读和写;
    O_APPEND:追加;
    O_CREAT:创建;
    O_EXCL:文件是否存在;
    O_TRUNC:截断;
    O_NONBLOCK:非阻塞;

    mode:为创建新文件设置权限参数,权限受到umask影响(默认文件操作权限);
    //例如:rw-r--r-- 0644
    //文件权限=mode&(~umask)

    //返回值
    成功:返回新的文件描述符;

    失败:-1,errno;
  • int close(int fd)关闭打开的文件描述符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <unistd.h>

    int close(int fd);

    //参数
    fd:要关闭文件的文件描述符;


    //返回值
    成功:0;

    失败:-1,errno;

open常见错误

  • 打开文件不存在(文件不存在,并且不设置创建参数)
  • 以写方式打开只读文件(打开文件没有对应权限)
  • 以只写方式打开目录

read/write函数

函数原型

  • ssize_t read(int fd,void *buf,size_t count):原本是用于文件读取的操作,但是也可以用与socket的接收以及读取数据的操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <unistd.h>

    ssize_t read(int fd,void *buf,size_t count);

    //参数
    fd:要操作的文件描述符;

    buf:是一个指向读或写数据的缓冲区指针;

    count:缓冲区长度;


    //返回值
    成功:返回读到的字节数,0,表示已经到达文件末尾;

    失败:-1,errno;
    -1:
    - 如果read返回-1并且errno等于EAGIN或EWOULDBLOCK,说明不是read失败而是read以非阻塞读文件并且文件无数据
    - 如果errno==EINTR,被异常终止,需要重启
    - 如果errno==ECONNRESET,说明连接被重置

    //创建缓冲区,可以使用BUFSIZ,BUFSIZ=4096
    char buf[BUFSIZ];
  • ssize_t write(int fd,const void *buf,size_t count)用于进行文件写数据的操作,但是也可以用于socket通信的发送以及写数据的操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <unistd.h>

    ssize_t write(int fd,const void *buf,size_t count);

    //参数
    fd:要操作的文件描述符;

    buf:待写出数据的缓冲区;

    count:数据大小;

    //返回值
    成功:返回写入的字节数;

    失败:-1,errno;

实现cp命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <fcntl.h>

int main (int argc, char *argv[]) {
char buf[4096];
int n=0;
int fd1=open(argv[1],O_RDONLY);
if(fd1==-1){
perror("open argv1 error");
exit(1);
}
int fd2=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664);
if(fd2==-1){
perror("open argv2 error");
exit(1);
}
while((n=read(fd1,buf,1024))!=0){
if(n<0){
perror("read error");
break;
}
write(fd2,buf,n);
}
close(fd1);
close(fd2);

return 0;
}

//运行
//g++ test.cpp -o test
//./test add.h add1.h
//发现add1.h的内容跟add.h一样

lseek函数

函数原型

off_t lseek(int fd, off_t offset, int whence)用于读写文件偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);

//参数
fd:文件描述符;

offset:偏移量;

whence:起始偏移位置;
//whence参数选择
SEEK_SET:文件开头;
SEEK_CUR:当前位置;
SEEK_END:文件末尾;

//返回值
成功:较起始位置向后偏移量;

失败:-1,errno;

注:

  • lseek同样允许超过文件结尾设置偏移量,文件会因此扩展
  • 注意文件”读”和”写”使用同一偏移位置

应用场景:

  • 文件的”读”和”写”使用同一偏移位置
  • 使用lseek获取文件大小(lseek(fd,0,SEEK_END);)
  • 使用lseek扩展文件大小,要想使文件大小真正扩展,必须引起IO操作

错误处理函数

概念

  • Linux中也提供了一种用来查看错误原因的变量以及函数
  • errno是Linux中的一个全局变量,当你程序发生报错时,会进行设置errno来告诉你报错的原因,而errno是一个整数,这个整数会对应着一个错误原因
  • strerror函数是可以解读errno来返回这个errno所对应的原因
  • perror函数是可以设置报错提示,并且将errno的错误原因一起输出

函数原型

  • char *strerror(int errnum)可以解读errno来返回这个errno所对应的原因

    1
    2
    3
    4
    5
    6
    #include <string.h>

    char *strerror(int errnum);

    //参数
    errnum:errno;
  • void perror(const char *s)可以设置报错提示,并且将errno的错误原因一起输出

    1
    2
    3
    4
    5
    6
    7
    8
    #include <stdio.h>
    //#include <cstdio>

    void perror(const char *s);


    //参数
    s:要设置的报错提示;

阻塞、非阻塞

概念

  • 阻塞:当进程调用一个阻塞的系统函数时,该进程将会置于睡眠状态,这时内核调度其他进程运行,直到该进程等待的时间发生(比如网络上接收到包,或者调用sleep指定的睡眠时间到了),它才可能继续运行
  • 非阻塞:当调用非阻塞的系统函数时,如果不能立即得到结果,则不会阻塞当前线程或进程,但是调用者需要定时轮询查看处理状态
  • 产生阻塞的场景:读设备文件(dev目录)、读网络文件等等
  • 阻塞、非阻塞是文件的属性,而文件属性是可以修改的

阻塞示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>


int main(){
char buf[10];
int n;
n=read(STDIN_FILENO,buf,10);
if(n<0){
perror("read STDIN_FILENO");
exit(1);
}
write(STDOUT_FILENO,buf,n);
return 0;
}


//如果我不按键盘输入东西,它将会一直等待也就是阻塞

阻塞示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main(){
char buf[10];
int fd,n;
fd=open("/dev/tty",O_RDONLY|O_NOBLOCK);
if(fd<0){
perror("open /dev/tty");
exit(1);
}

tryagain:
n=read(fd,buf,10);
if(n<0){
if(errno!=EAGAIN){
perror("read /dev/tty");
exit(1);
}else{
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2);
goto tryagain;
}
}
write(STDOUT_FILENO,buf,n);
close(fd);

return 0;
}

//虽然设置了非阻塞文件属性,但是没有设置超时限制和结束限制,导致一直阻塞

非阻塞示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\n"

int main(){
char buf[10];
int fd,n,i;
fd=open("/dev/tty",O_RDONLY|O_NONBLOCK);
if(fd<0){
perror("open /dev/tty");
exit(1);
}
printf("open /dev/tty ok... %d\n",fd);

for(i=0;i<5;++i){
n=read(fd,buf,10);
if(n>0) break;
if(errno!=EAGAIN){
perror("read /dev/tty");
exit(1);
}else{
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
sleep(2);
}
}
if(i==5){
write(STDOUT_FILENO,MSG_TIMEOUT,strlen(MSG_TIMEOUT));
}else{
write(STDOUT_FILENO,buf,n);
}

close(fd);
return 0;
}


//这种方式也不是最优的,最优的应该为响应的模式,当你们有需求我才响应,而不是我以轮询的方式去查看

fcntl函数

概念

  • fcntl函数可以用来改变一个已经打开的文件的访问控制属性

函数原型

int fcntl(int fildes,int cmd,.../* arg */):可以用来改变一个已经打开的文件的访问控制属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <fcntl.h>

int fcntl(int fildes, int cmd, .../* arg */);

//参数
fildes:要操作的文件描述符;

cmd:函数行为参数;
//行为参数选择:
F_GETFL:获取文件属性;
F_SETFL:设置文件属性;

...:可变参数,也就是设置文件属性时可以加入文件属性作为第三个参数;


//返回值
F_GETFL:会返回文件属性;

F_SETFL:-1以外的值;

文件操作

stat/lstat函数

函数原型

  • int stat(const char *path,struct stat *buf)获取文件属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/stat.h>

int stat(const char *path,struct stat *buf);

//参数
path:文件路径或文件名;

buf:inode结构体指针,传出参数;



//返回值
成功:0;

失败:-1,errno;
  • int lstat(const char *path,struct stat *buf)获取文件属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/stat.h>

int lstat(const char *path,struct stat *buf);

//参数
path:文件路径或文件名;

buf:inode结构体指针,传出参数;



//返回值
成功:0;

失败:-1,errno;

inode结构体:

image-20240425122528875

应用场景

  • buf.st_size:获取文件大小
  • buf.st_mode:获取文件类型
  • buf.st_mode:获取文件权限

判断文件类型

  • 我们可以通过st_mode中给出的一些宏来进行判断文件类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    st_mode给出的宏:
    m:st_mode
    1.S_ISREG(m):判断文件是不是普通文件;
    2.S_ISDIR(m):判断文件是不是目录;
    3.S_ISCHR(m):判断文件是不是设备文件;
    4.S_ISBLK(m):判断是否为块设备文件;
    5.S_ISFIFO(m):判断是否为管道;
    6.S_ISLNK(m):判断是否为符号链接;
    7.S_ISSOCK(m):判断是否为soket套接字;
  • 使用方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <cstdio>
    #include <cstdlib>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>

    int main (int argc, char *argv[]) {
    struct stat sb;
    int ret=stat(argv[0],&sb);
    if(ret==-1){
    perror("stat error");
    exit(1);
    }
    if(S_ISREG(sb.st_mode)){
    printf("It's a regular\n");
    }
    return 0;
    }

    注:stat是不能够穿透符号链接,而lstat是能够穿透符号链接,也就是说当用stat对符号链接文件操作时,判断文件类型确是普通文件,而用lstat对符号链接文件操作时,判断文件类型确是符号链接

函数原型

  • int link(const char *oldpath,const char *newpath)可以为已经存在的文件创建目录项(硬链接)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

int link(const char *oldpath, const char *newpath);


//参数
oldpath:要创建硬链接文件路径;

newpath:硬链接文件保存的地址;

//返回值
成功:0;

失败:-1,errno;
  • int unlink(const char *pathname)删除一个文件的目录项(硬链接)
1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>

int unlink(const char *pathname);

//参数
pathname:要删除目录项的文件路径;

//返回值
成功:0;

失败:-1,errno;

注:

  • Linux删除文件的机制,是不断将inode中的st_nlink-1,直至减到为0为止。无目录项对应的文件,将会被操作系统择机释放
  • unlink函数特征清除文件时,如果文件的硬链接数到0了,没有dentry对应,则该文件仍不会马上被释放。要等到所有打开该文件的进程关闭该文件,系统才会择机将该文件释放掉

实现mv命令

1
2
3
4
5
6
7
8
#include <unistd.h>


int main (int argc, char *argv[]) {
link(argv[1], argv[2]);
unlink(argv[1]);
return 0;
}

隐式回收

概念当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空 间会被释放,这一特性称之为隐式回收系统资源

symlink函数

函数原型

int symlink(const char *oldpath,const char *newpath)用于创建文件的软链接

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>

int symlink(const char *target, const char *linkpath);

//参数
target:要创建软连接的文件目标地址;

linkpath:软连接文件保存地址;

//返回值
成功:0;

失败:-1,errno;

readlink函数

函数原型

ssize_t readlink(const char *path,char *buf,size_t bufsiz)用于读取符号链接(软链接)文件内容,得到链接所指向的文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>

ssize_t readlink(const char *restrict pathname, char *restrict buf,size_t bufsiz);


//参数
pathname:要读取内容的软链接文件路径;

buf:将读入的内容保存的缓冲区;

bufsiz:缓冲区大小;


//返回值
成功:返回实际读到的字节数;

失败:-1,errno;

rename函数

函数原型

int rename(const char *oldpath,const char *newpath)为文件重命名

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int rename(const char *oldpath, const char *newpath);


//参数
oldpath:原来的文件名;

newpath:新的文件名;

目录操作

getcwd函数

函数原型:

char *getcwd(char *buf,size_t size)获取进程当前工作目录(man卷3),相当于pwd命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

char *getcwd(char *buf, size_t size);


//参数
buf:将获取到的信息存入的缓冲区;

size:缓冲区大小;

//返回值
成功:buf中保存当前进程工作目录位置;

失败:NULL;

chdir函数

函数原型:

int chdir(const char *path)改变当前进程的工作目录,其实就是cd命令

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>

int chdir(const char *path);

//参数
path:要进入的工作目录;

//返回值
成功:0;

失败:-1,errno;

opendir/closedir函数

函数原型:

  • DIR *opendir(const char *name)根据传入的目录名打开一个目录,DIR*类似于FILE*
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <dirent.h>

DIR *opendir(const char *name);


//参数
name:目录名;
//参数支持相对路径、绝对路径

//返回值
成功:返回指向该目录结构体指针;

失败:返回NULL;
  • int closedir(DIR *dirp)关闭指定打开的目录
1
2
3
4
5
6
7
8
9
10
11
12
#include <dirent.h>

int closedir(DIR *dirp);


//参数
dirp:目录名;

//返回值
成功:0;

失败:-1,errno;

readdir函数

函数原型:

  • struct dirent *readdir(DIR *dirp)读取指定目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <dirent.h>

struct dirent *readdir(DIR *dirp);


//参数
dirp:目录名;

//返回值
成功:返回目录项结构体指针;

失败:返回NULL,errno;
//注意:读取数据结束时也返回NULL值,所以需要借助errno进一步区分


//dirent结构体
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};

rewinddir函数

函数原型:

void rewinddir(DIR *dirp)回卷目录读写位置至起始位置

1
2
3
4
5
6
#include <dirent.h>

void rewinddir(DIR *dirp);

//参数
dirp:目录名;

telldir/seekdir函数

函数原型:

  • long telldir(DIR *dirp)获取目录读写位置
1
2
3
4
5
6
7
8
9
10
11
#include <dirent.h>

long telldir(DIR *dirp);

//参数
dirp:目录名;

//返回值
成功:与dirp相关的目录当前读写位置;

失败:-1,errno;
  • void seekdir(DIR *dirp,long loc)修改/跳转目录指定的读写位置
1
2
3
4
5
6
7
8
#include <dirent.h>

void seekdir(DIR *dirp, long loc);

//参数
dirp:目录名;

loc:一般telldir的返回值决定;

递归遍历目录

思路

  1. 判断命令行参数,获取用户要查询的目录名(argv[1]),还需要判断argc==1->./目录
  2. 判断用户指定的是否是目录,使用stat S_ISDIR()
  3. 读目录,opendir(),readdir(),closedir()
  4. 递归读取目录文件,普通文件直接打印,目录就拼接出绝对路径,递归调用opendir()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <cstddef>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>


#define PATH_LEN 256

//第二个参数为函数指针
void fetchdir(const char *dir,void(*fcn)(char *)){
char name[PATH_LEN];
struct dirent *sdp;
DIR *dp;

if((dp=opendir(dir))==NULL){
fprintf(stderr,"fetchdir:can't open %s\n",dir);
return;
}
//读取目录项
while ((sdp=readdir(dp))!=NULL) {
if(strcmp(sdp->d_name,".")==0||strcmp(sdp->d_name,"..")==0) continue;
if(strlen(dir)+strlen(sdp->d_name)+2>sizeof(name)){
fprintf(stderr,"fetchdir:name %s %s too long\n",dir,sdp->d_name);
}else {
sprintf(name,"%s/%s",dir,sdp->d_name);
//利用函数指针调用函数
(*fcn)(name);
}
}

closedir(dp);
}

//处理目录/文件
void isfile(char *name){
struct stat sbuf;

//文件名无效
if(stat(name,&sbuf)==-1){
fprintf(stderr, "isfile:can't access %s\n",name);
exit(1);
}
//判断是否为目录
if((sbuf.st_mode&S_IFMT)==S_IFDIR) fetchdir(name,isfile);

printf("%8ld %s\n",sbuf.st_size,name); //不是目录,则是普通文件,直接打印文件名
}


int main (int argc, char *argv[]) {
if(argc==1) isfile(".");
else{
while (--argc>0) { //可一次查询多个目录
isfile(*++argv); //循环调用该函数处理各个命令行传入的目录
}
}
return 0;
}

重定向

函数原型

  • int dup(int oldfd)将已有的文件描述符文件里的内容复制到新建的文件描述符文件中
1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>

int dup(int oldfd);

//参数
oldfd:已有文件描述符;

//返回值
成功:新文件描述符;

失败:-1,errno;
  • int dup2(int oldfd,int newfd)将一个文件描述符newfd重定向到指定文件描述符oldfd的文件,重定向完,对newfd所指向文件操作也就是对oldfd所指向文件操作
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>

int dup2(int oldfd, int newfd);

//参数
oldfd:已有文件描述符;

newfd:要重定向的文件描述符;

//返回值
成功:返回newfd;

失败:-1,errno;

注:对于dup2,如果oldfd不是有效的文件描述符,会报错,但是newfd文件描述符不会关闭

fcntl实现dup

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <pthread.h>

int main (int argc, char *argv[]) {
int fd1=open(argv[1],O_RDWR);
printf("fd1 = %d\n",fd1);
int newfd=fcntl(fd1,F_DUPFD,0); //0被占用,fcntl使用文件描述符表中可用的最小文件描述符返回
//也可以指定一个<1024的文件描述符x,如果文件描述符没有被占用就可以直接使用,如果被占用系统自动找寻大于等于x没有被占用的文件描述符
printf("newfd = %d\n",newfd);
return 0;
}

进程控制

fork函数

函数原型

pid_t fork(void)创建一个子进程

1
2
3
4
5
6
7
8
9
#include <unistd.h>

pid_t fork(void);


//返回值
成功:父进程接收到返回值新子进程ID(PID),创建出来的子进程接收到返回值0;

失败:父进程接收到-1,errno,子进程没被创建成功;

创建子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>



int main (int argc, char *argv[]) {
//用来检测子进程创建后是否是从头运行还是从fork()开始
//应该只打印一次,因为子进程会从fork()开始
std::cout<<"before fork -1"<<std::endl;


pid_t pid=fork();
//子进程创建失败
if(pid==-1){
perror("fork error");
exit(1);
}else if (pid==0) {
//说明当前进程是子进程
std::cout<<"child is created"<<std::endl;
}else if (pid>0) {
//说明当前进程为父进程
std::cout<<"parent process: my child is "<<pid<<std::endl;
}

std::cout<<"end of file"<<std::endl;
return 0;
}

getpid/getppid函数

函数原型:

  • pid_t getpid(void)获取当前进程ID(PID)
1
2
3
4
5
6
7
8
#include <unistd.h>

pid_t getpid(void);

//返回值
成功:当前进程PID;

失败:无返回值,因为没有定义;
  • pid_t getppid(void)获取当前子进程的父进程ID(PID)
1
2
3
4
5
6
7
8
#include <unistd.h>

pid_t getppid(void);

//返回值
成功:返回当前子进程的父进程PID;

失败:无返回值,因为没有定义;

循环创建子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <iostream>



int main (int argc, char *argv[]) {
int i;
for(i=0;i<5;++i){
pid_t pid=fork();
//子进程创建失败
if(pid==-1){
perror("fork error");
exit(1);
}else if (pid==0) {
break;
}
}
if(5==i) {
sleep(5);
std::cout<<"I'm parent \n";
}
else {
sleep(i);
std::cout<<"I'm child"<<i+1<<std::endl;
}
return 0;
}

getuid/getgid函数

函数原型:

  • uid_t getuid(void)获取当前进程实际用户ID
1
2
3
4
5
6
7
8
9
#include <unistd.h>

uid_t getuid(void);


//返回值
成功:当前进程实际用户ID;

失败:无返回值,因为没有定义;
  • uid_t geteuid(void)获取当前进程有效用户ID
1
2
3
4
5
6
7
8
#include <unistd.h>

uid_t geteuid(void);

//返回值
成功:当前进程有效用户ID;

失败:无返回值,因为没有定义;
  • gid_t getgid(void)获取当前进程使用用户组ID
1
2
3
4
5
6
7
8
9
#include <unistd.h>

gid_t getgid(void);


//返回值
成功:当前进程使用用户组ID;

失败:无返回值,因为没有定义;
  • gid_t getegid(void)获取当前进程有效用户组ID
1
2
3
4
5
6
7
8
#include <unistd.h>

gid_t getegid(void);


成功:当前进程有效用户组ID;

失败:无返回值,因为没有定义;

进程共享

概念:

  • 父子进程共享遵循读时共享写时复制,例如:有一个全局变量cnt=100,当我父进程或者子进程读cnt时,将会共享这个cnt的值也就是100,但是当父进程或子进程要更改其值也就是写时,将会给当前进程copy一个cnt过来再改,因此当父进程或子进程修改cnt(只修改自己进程中的cnt),子进程或父进程读cnt得到的值还是cnt

  • 父子进程不会共享全局变量

  • 父子进程相同处:刚fork后,data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式

  • 父子进程不同处:进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集

  • 父子进程共享

    1. 文件描述符
    2. mmap映射区

exec函数族

概念

  • exec函数族可以让子进程或父进程执行指定的某个可执行程序
  • 当进程调用exec时,则进程中的代码段将会换成你要exec的可执行程序的代码段,以此来执行指定程序,但是进程ID没变
  • exec不能返回的!

execl函数

int execl(const char *path,const char *arg,...)加载一个进程(可执行程序),用于加载普通和系统的可执行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>

int execl(const char *path,const char *arg,...);


//参数
path:可执行文件路径;

arg:argv[0],所以跟path一样;

...:用来传入执行可执行文件的参数,可变参数;
//注意:结尾得传入NULL来作为哨兵来结束参数输入

//返回值
失败:-1,errno;

execlp函数

int execlp(const char *file,const char *arg,...)借助PATH环境变量加载一个进程,用于加载系统可执行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

int execlp(const char *file,const char *arg,...);

//参数
file:可执行文件的文件名;

arg:argv[0],所以还是传入可执行文件名;

...:用来传入执行可执行文件的参数,可变参数;
//注意:结尾得传入NULL来作为哨兵来结束参数输入

//返回值
失败:-1,errno;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>



int main (int argc, char *argv[]) {
pid_t pid=fork();
if(pid==-1){
perror("fork error");
exit(1);
}else if (pid==0) {
execlp("ls", "ls","-l","-h",NULL);
//因为只有报错时才会返回值,所以不需要判断返回值
perror("exec error");
exit(1);
}else if (pid>0) {
sleep(1);
std::cout<<"I'm parent:"<<getpid()<<std::endl;
}
return 0;
}

//运行结果:将会打印当前目录下文件信息

execvp函数

int execvp(const char *file,const char *argv[])使用自定义环境变量env加载一个进程

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>

int execvp(const char *file,const char *argv[]);


//参数
file:可执行文件的文件名;

argv[]:可变参数,类似于main函数中的argv[];

//返回值
失败:-1,errno;

exec函数族特点

  • exec函数一旦调用成功即执行新的程序,只有失败才返回!

  • 函数名中的字母含义:

    1. l (list):命令行参数列表
    2. p (path):搜索file时使用path变量
    3. v (vector):使用命令行参数数组
    4. e (environment):使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量
  • 只有exeve函数是系统调用函数

回收子进程

引言

孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程

僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程

注意:

  • 僵尸进程是不能使用kill命令清除掉的,因为kill命令只是用来终止进程的,而僵尸进程已经终止
  • 需要使用kill命令将父进程kill掉,然后由init接收僵尸进程并回收

概念

  • 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在按其中保存了一些信息,如果是正常退出则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个
  • 这个进程的父进程可以调用wait或waitpid获取这些信息,彻底清除掉这个进程
  • wait、waitpid都只能够一次回收一个进程

wait函数

函数原型

pid_t wait(int *status):父进程调用wait函数可以回收子进程终止信息,有三个功能:

  1. 阻塞等待子进程退出(死亡)
  2. 回收子进程残留资源
  3. 获取子进程结束状态(退出原因)
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/wait.h>

pid_t wait(int *status);

//参数
status:传出参数,子进程退出状态;



//返回值
成功:返回被回收子进程的进程ID;

失败:-1;

子进程退出信息

要想进一步获取退出状态和原因需要借助一些宏函数

  • WIFEXITED(status):判断子进程是否正常终止,为真则正常终止WEXITSTATUS(status):只有当子进程是正常终止才调用查看子进程返回值

  • WIFSIGNALED(status):判断子进程是否是被信号终止的,为真为是

    WTERMSIG(status):只有当子进程是被信号终止时,用来查看子进程是被哪个信号终止

  • WIFSTOPPED(status):判断子进程是否被信号阻塞,为真则是

    WSTOPSIG(status):只有当子进程是被信号阻塞时,用来查看子进程被哪个信号阻塞的

    WIFCONTINUED(status):查看子进程是否被恢复

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <cstdio>
#include <cstdlib>
#include <ostream>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <sys/wait.h>


int main (int argc, char *argv[]) {
pid_t pid,wpid;
pid=fork();

int status;
if(pid==0){
std::cout<<"---child,my id="<<getpid()<<",going to sleep 10s\n";
sleep(10);
std::cout<<"--------child die--------"<<std::endl;
return 73;
}else if (pid>0) {
//wpid=wait(NULL); //不关心子进程结束原因
wpid=wait(&status); //如果子进程未终止,父进程阻塞在这个函数上
if(wpid==-1){
perror("wait error");
exit(1);
}
if(WIFEXITED(status)){
std::cout<<"child exit with "<<WEXITSTATUS(status)<<std::endl;
}
if(WIFSIGNALED(status)){
std::cout<<"child kill with signal "<<WTERMSIG(status)<<std::endl;
}
std::cout<<"I am parent,pid="<<getgid()<<",myson="<<pid<<std::endl;
std::cout<<"被回收的进程ID="<<wpid<<std::endl;
sleep(1);
}else {
perror("fork error");
return 1;
}
return 0;
}

waitpid函数

函数原型

pid_t waitpid(pid_t pid,int *status,int options)作用同wait一样(父进程调用wait函数可以回收子进程终止信息),但可指定pid进程清理,可以不阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <sys/wait.h>

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

//参数
pid:要被回收的子进程ID;
//参数列表:
> 0:回收指定ID的子进程;
-1:回收任意子进程(相当于wait);
0:回收和当前调用waitpid一个组的所有子进程;
< -1:回收指定进程组内的任意子进程;

status:传出参数,子进程退出状态;

options:函数行为标志位参数;
//标志位参数有,可以看第二卷man手册:
0:阻塞;
WNOHANG:不阻塞;





//返回值
返回值>0:表示成功回收的子进程pid;

返回值=0:函数调用时,参数三指定WNOHANG,并且没有子进程结束;

失败:-1,errno;

waitpid回收多个进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <cstdio>
#include <cstdlib>
#include <ostream>
#include <sys/types.h>
#include <unistd.h>
#include <iostream>
#include <sys/wait.h>


int main (int argc, char *argv[]) {
int i;
pid_t pid,wpid;
pid=fork();

for(i=0;i<5;++i){
pid=fork();
if(pid==0) break;
}

if(5==i){
/* while((wpid=waitpid(-1,NULL,0))){ //使用阻塞方式回收子进程
std::cout<<"wait child "<<wpid<<std::endl;
} */
//使用非阻塞
while((wpid=waitpid(-1,NULL,WNOHANG))!=-1){
if(wpid>0){
std::cout<<"wait child "<<wpid<<std::endl;
}else if (wpid==0) {
sleep(1);
continue;
}
}
}else {
sleep(i);
std::cout<<"I'm "<<i+1<<"th child,pid="<<getpid()<<std::endl;
}
return 0;
}

进程间通信IPC

概念

  • 进程间通信(IPC)也就是进程与进程之间进行通信、共享和数据传递 ,叫作进程间通信
  • 进程间通信的实质就是在两个进程间放置一个缓冲区,让两个进程在缓冲区里进行读和写来进行通信。

进程间通信方式

  • 管道(使用最简单)
  • 信号(开销最小)
  • mmap共享映射区(无血缘关系)
  • 本地套接字socket(最稳定)

管道

概念

概念

  • 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。
  • 管道的本质是一个伪文件(实为内核缓冲区)
  • 由两个文件描述符引用的,一个表示读端,一个表写端
  • 管道是内核使用环形队列机制,借助内核缓冲区实现的

局限性

  • 数据不能自己写,自己读
  • 管道中数据不可反复读取,一旦读走,管道中的相应内容不再存在
  • 采用半双工通信方式,数据只能在单方向上流动

特点

  1. **它是半双工的(即数据只能在一个方向上流动)**,具有固定的读端和写端
  2. 只能用于具有亲缘关系的进程之间的通信(父子进程或兄弟进程之间)
  3. 它可以看成是一种特殊的文件(伪文件),对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中

pipe函数

函数原型

int pipe(int pipefd[2])创建并打开管道

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <unistd.h>

int pipe(int pipefd[2]);

//参数
pipefd[0]:读端的文件描述符;

pipefd[1]:写端的文件描述符;

//返回值
成功:0;

失败:-1,errno;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>

//父进程写,子进程读,则父进程需要关闭读端,子进程需要关闭写端
int main (int argc, char *argv[]) {
int ret,fd[2];
pid_t pid;
char *str="hello pipe\n";
char buf[1024];
ret=pipe(fd);
if(ret==-1){
perror("pipe error");
exit(1);
}
pid=fork();
if(pid>0){
close(fd[0]); //父进程关闭读端
write(fd[1],str,strlen(str));
sleep(1);
close(fd[1]); //写完数据就关闭
}else if (pid==0) {
close(fd[1]); //子进程关闭写端
ret=read(fd[0],buf,sizeof(buf));
write(STDOUT_FILENO, buf, ret);
close(fd[0]); //读完数据就关闭读端
}
return 0;
}

管道的读写行为

  1. 读管道

    1. 管道中有数据,read返回实际读到的字节数.
    2. 管道中无数据:
      • 管道写端被全部关闭,read返回0
      • 写端没有全部关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
  2. 写管道

    1. 管道读端全部被关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
    2. 管道读端没有全部关闭:
      • 管道已满,write阻塞
      • 管道未满,write将数据写入,并返回实际写入的字节数

兄弟进程通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>

//父进程写,子进程读,则父进程需要关闭读端,子进程需要关闭写端
int main (int argc, char *argv[]) {
int ret,fd[2],i;
pid_t pid;
ret=pipe(fd);
if(ret==-1){
perror("pipe error");
exit(1);
}

for(i=0;i<2;++i){
pid=fork();
if(pid==-1){
perror("fork error");
exit(1);
}
if(pid==0) break;
}

if(i==2){
close(fd[0]); //关闭父进程的读端
close(fd[1]); //关闭父进程的写端
wait(NULL);
wait(NULL);
}else if (i==0) { //兄
close(fd[0]); //关闭兄进程的读端
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perror("execlp ls error");
exit(1);
}else if (i==1) { //弟
close(fd[1]); //关闭弟进程的写端
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perror("execlp ls error");
exit(1);
}

return 0;
}

FIFO

概念

概念

  • FIFO(命名管道)是Linux基础文件类型中的一种各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就是实现了进程间通信
  • FIFO文件在磁盘上没有数据块,仅仅用来标识内核中的一条通道
  • 在Linux终端中可以使用mkfifo命令,进行创建FIFO文件

特点

  1. FIFO可以在无血缘关系的进程之间交换数据,与pipe不同。
  2. 它以一种特殊设备文件形式存在于文件系统中,所以我们应该用对文件的操作对待它

mkfifo函数

函数原型

int mkfifo(const char *pathname,mode_t mode)用来创建FIFO文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

//参数
pathname:fifo文件路径名;

mode:为创建新文件设置权限参数,权限受到umask影响(默认文件操作权限);
//例如:rw-r--r-- 0644
//文件权限=mode&(~umask)

//返回值
成功:0;

失败:-1,errno;

FIFO进程通信

先创建一个myfifo的fifo文件

写端:test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char *argv[]) {
int fd,i;
char buf[BUFSIZ];
if(argc<2){
std::cout<<"Enter like this: ./a.out fifoname\n";
return -1;
}
//int fd=mkfifo("myfifo",0644);
fd=open(argv[1],O_WRONLY);
if(fd<0){
perror("open error");
exit(1);
}
i=0;
while (1) {
sprintf(buf,"hello itcast %d\n",i++);
write(fd,buf,strlen(buf));
sleep(1);
}
close(fd);
return 0;
}

读端:testr.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main (int argc, char *argv[]) {
int fd,len;
char buf[BUFSIZ];
if(argc<2){
std::cout<<"./a.out fifoname\n";
return -1;
}
fd=open(argv[1],O_RDONLY);
if(fd<0){
perror("open error");
exit(1);
}
while (1) {
len=read(fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);
sleep(3); //多个读端时应增加睡眠秒数,放大效果
}
close(fd);
return 0;
}

共享存储映射

存储映射I/O

概念

  • 存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射,当从缓冲区取数据相当于读文件中相应字节,将数据存入缓冲区,则相应的字节就自动写入文件。
  • 存储映射可以在不适用read和write函数的情况下,使用地址(指针)完成I/O操作

image-20240429083055810

使用注意事项

  1. 用于创建映射区的文件大小为0,实际指定非0大小创建映射区,出”总线错误(SIGBUS)”
  2. 用于创建映射区的文件大小为0,实际指定0大小创建映射区,出”无效参数”
  3. 用于创建映射区文件的读写属性为只读,映射区属性为读写,出”无效参数”
  4. 创建映射区需要读权限,当访问权限指定为MAP_SHARED时,mmap的读写权限应该<=文件的open权限
  5. 文件描述符fd,在mmap创建映射区完成即可关闭,后续访问文件,用地址访问
  6. offset必须是4096的整数倍(MMU映射的最小单位为4k)
  7. 对申请的映射区内存,不能越界访问。
  8. munmap用于释放的地址,必须是mmap申请返回的地址
  9. 映射区访问权限为MAP_PRIVATE,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上
  10. 映射区访问权限为MAP_PRIVATE,只需要open文件时,有读权限用于创建映射区即可

mmap函数

函数原型

void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset)创建共享内侧映射区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags,int fd, off_t off);


//参数
addr:建立映射区的首地址,由Linux内核指定,使用时,直接传入NULL;

length:欲创建映射区的大小(小于等于文件实际大小);

prot:映射区权限;
//选择:
PROT_READ:读;
PROT_WRITE:写;
PROT_READ|PROT_WRITE:读写;
PROT_NONE:没有访问权限;
...:更多的看man手册第二卷的mmap函数;

flags:标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区);
//选择:
MAP_SHARED:会将映射区所做的操作反映到物理设备(磁盘)上;
MAP_PRIVATE:映射区所做的修改不会反映到物理设备;
...:更多的看man手册第二卷的mmap函数;

//需要先有文件
fd:用于创建共享内存映射区的那个文件的文件描述符;

offset:默认0,表示映射文件全部。偏移位置,取值必须是4096的整数倍;

//返回值
成功:共享内存映射区的首地址;

失败:返回MAP_FAILED((void *)-1),errno;
//返回一个将-1强转成void*的宏

mmap函数保险调用方式

  1. open(pathname,O_RDWR,0644);
  2. mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,fd,0);

munmap函数

函数原型

int munmap(void *addr,size_t length)释放共享内存映射区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <sys/mman.h>

int munmap(void *addr,size_t length);


//参数
addr:要释放的映射区的首地址,也是mmap函数的返回值;

length:映射区大小;

//返回值
成功:0;

失败:-1,errno;

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main (int argc, char *argv[]) {
char *p=nullptr;
int fd;
fd=open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd==-1){
perror("open error");
exit(1);
}
/* lseek(fd,10,SEEK_END);
write(fd,"\0",1); */
ftruncate(fd,20); //可以代替lseek操作来扩展文件
int len=lseek(fd,0,SEEK_END);
p=(char *)mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p==MAP_FAILED){
perror("mmap error");
exit(1);
}

//使用p对文件进行读写操作
strcpy(p,"hello mmap");
std::cout<<"-------"<<p<<std::endl;

int ret=munmap(p,len);
if(ret==-1){
perror("munmap error");
exit(1);
}
close(fd);
return 0;
}

mmap父子进程通信

概念

  • 父子等有血缘关系的进程之间也可以通过mmap建立映射区来完成数据通信,但相应的要在创建映射区的时候指定对应的标志位参数flags:

    • MAP_PRIVATE:父子进程各自独占映射区(私有映射)
    • MAP_SHARED:父子进程共享映射区(共享映射)
  • 要想父子进程通信,则flags应该置于MAP_SHARED

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <fcntl.h>
#include <iostream>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>


int var=100;


int main (int argc, char *argv[]) {
int *p;
pid_t pid;
int fd;
fd=open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd<0){
perror("open error");
exit(1);
}
// unlink("testmap"); //删除临时文件目录项,使之具备被释放的条件
ftruncate(fd,4);
p=(int *)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
// p=(int *)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0);
if(p==MAP_FAILED){
perror("mmap error");
exit(1);
}
close(fd);
pid=fork();
if(pid==0){
*p=2000; //写共享内存,因为映射区为int*类型所以,只能写整形变量
var=1000;
std::cout<<"child,*p="<<*p<<",var="<<var<<std::endl;
}else if(pid>0){
sleep(1);
std::cout<<"parent,*p="<<*p<<",var="<<var<<std::endl;
wait(NULL);
int ret=munmap(p, 4);
if(ret==-1){
perror("munmap error");
exit(1);
}
}else {
perror("fork error");
exit(1);
}
return 0;
}
//如果子进程和父进程输出的*p一致说明他们能通信
//使用私有权限不能够通信

mmap无血缘关系进程通信

示例:

写端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>

struct student{
int id;
char name[256];
int age;
};


int main (int argc, char *argv[]) {
int fd;
student stu={10,"小明",18},*p;
fd=open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd==-1){
perror("open error");
exit(1);
}

ftruncate(fd,sizeof(stu));
p=(struct student*)mmap(NULL,sizeof(stu),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p==MAP_FAILED){
perror("mmap error");
exit(1);
}
close(fd);
while (1) {
memcpy(p,&stu,sizeof(stu));
stu.id++;
sleep(1);
}
int ret=munmap(p,sizeof(stu));
if(ret==-1){
perror("munmap error");
exit(1);
}

return 0;
}

读端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>

struct student{
int id;
char name[256];
int age;
};


int main (int argc, char *argv[]) {
int fd;
student stu,*p;
fd=open("testmap",O_RDONLY);
if(fd==-1){
perror("open error");
exit(1);
}

p=(struct student*)mmap(NULL,sizeof(stu),PROT_READ,MAP_SHARED,fd,0);
if(p==MAP_FAILED){
perror("mmap error");
exit(1);
}
close(fd);
while (1) {
printf("id=%d,name=%s,age=%d\n",p->id,p->name,p->age);
sleep(1);
}
int ret=munmap(p,sizeof(stu));
if(ret==-1){
perror("munmap error");
exit(1);
}

return 0;
}

匿名映射

概念

  • 匿名映射可以不需要创建文件来创建共享内存映射区,在mmap函数的flags标志位参数添加上MAP_ANON或MAP_ANONYMOUS来实现,-1代替fd
  • 之前使用mmap映射时,我们都是使用创建一个文件来创建共享内存映射区,但是我们也可以使用匿名映射,来避免需要创建文件来创建共享内存映射区
  • 只能用于血缘关系进程间通信
  • 也可以使用open(“/dev/zero”)来当作映射区,因为/dev/zero可以提供很大的空间,你想要多少他就给多少,这样就不需要扩展文件,直接创建映射区(虽然不是匿名映射)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
int var=100;


int main (int argc, char *argv[]) {
int *p;
p=(int *)mmap(NULL,40,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(p==MAP_FAILED){
perror("mmap error");
exit(1);
}
pid_t pid=fork();
if(pid==0){
*p=7000;
var=1000;
printf("child,*p=%d,var=%d\n",*p,var);
}else {
sleep(1);
printf("parent,*p=%d,var=%d\n",*p,var);
wait(NULL);
}
int ret=munmap(p,4);
if(ret==-1){
perror("munmap error");
exit(1);
}

return 0;
}

信号

概念

概念

  • 信号(SIGNAL):给我们提供了一个信息,表示某件事件发生。在我们生活中随处可见,如:古代战争摔杯为号;体育比赛中的信号枪

  • 常用信号可以使用kill -l和man 7 signal来查看

  • 特点:

    1. 简单
    2. 不能携带大量信息
    3. 满足某个特设条件才发送

信号的机制

  • A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行
  • 由于信号是通过软件方法实现的,其实现手段导致信号有很强的延时性。但相对于用户来说,这个延迟时间非常短,不易察觉
  • 每个进程收到的所有信号,都是由内核负责发送的,内核处理

与信号相关的事件和状态

  • 产生信号状态

    1. 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
    2. 系统调用产生,如:kill、raise、abort
    3. 软件条件产生,如:定时器alarm
    4. 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
    5. 命令产生,如:kill命令
  • 递达状态:递达并且到达进程

  • 未决状态:产生和递达之间的状态,主要由于阻塞(屏蔽)导致该状态

  • 信号的处理方式

    1. 执行默认动作
    2. 忽略(丢弃)
    3. 捕捉(调用户处理函数)
  • **阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)**,在解除屏蔽前,一直处于未决态

  • 未决信号集

    1. 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。
    2. 信号产生后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态

信号四要素

  1. 信号编号
  2. 信号名称
  3. 信号对应事件
  4. 信号默认处理动作

注:信号使用之前,应先确定4要素,而后使用

默认处理动作

  • Term:终止进程
  • Ign:忽略信号(默认及时对该种信号忽略操作)
  • Core:停止进程,生成Core文件。(查验进程死亡原因,用于gdb调试)
  • Stop:停止(暂停)进程
  • Cont:继续运行进程

常规信号一览表

  1. SIGHUP:本信号在用户终端结束时发出,通常是在终端的控制进程结束时,通知同一会话期内的各个作业,这时他们与控制终端不在关联。比如,登录Linux时,系统会自动分配给登录用户一个控制终端,在这个终端运行的所有程序,包括前台和后台进程组,一般都属于同一个会话。当用户退出时,所有进程组都将收到该信号,这个信号的默认操作是终止进程。此外对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
  2. SIGINT:程序终止信号。当用户按下CRTL+C时通知前台进程组终止进程。
  3. SIGQUIT:Ctrl+\控制,进程收到该信号退出时会产生core文件,类似于程序错误信号。
  4. SIGILL:执行了非法指令。通常是因为可执行文件本身出现错误,或者数据段、堆栈溢出时也有可能产生这个信号。
  5. SIGTRAP:由断点指令或其他陷进指令产生,由调试器使用。
  6. SIGABRT:调用abort函数产生,将会使程序非正常结束。
  7. SIGBUS:非法地址。包括内存地址对齐出错。比如访问一个4个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法地址的非法访问触发。
  8. SIGFPE:发生致命的算术运算错误。
  9. SIGKILL:用来立即结束程序的运行。
  10. SIGUSR1:留给用户使用,用户可自定义。
  11. SIGSEGV:访问未分配给用户的内存区。或操作没有权限的区域。
  12. SIGUSR2:留给用户使用,用户可自定义。
  13. SIGPIPE:管道破裂信号。当对一个读进程已经运行结束的管道执行写操作时产生。
  14. SIGALRM:时钟定时信号。由alarm函数设定的时间终止时产生。
  15. SIGTERM:程序结束信号。shell使用kill产生该信号,当结束不了该进程,尝试使用SIGKILL信号。
  16. SIGSTKFLT:堆栈错误。
  17. SIGCHLD:子进程结束,父进程会收到。如果子进程结束时父进程不等待或不处理该信号,子进程会变成僵尸进程。
  18. SIGCONT:让一个停止的进程继续执行。
  19. SIGSTOP:停止进程执行。暂停执行。
  20. SIGTSTP:停止运行,可以被忽略。Ctrl+z。
  21. SIGTTIN:当后台进程需要从终端接收数据时,所有进程会收到该信号,暂停执行。
  22. SIGTTOU:与SIGTTIN类似,但在写终端时产生。
  23. SIGURG:套接字上出现紧急情况时产生。
  24. SIGXCPU:超过CPU时间资源限制时产生的信号。
  25. SIGXFSZ:当进程企图扩大文件以至于超过文件大小资源限制时产生。
  26. SIGVTALRM:虚拟使用信号。计算的是进程占用CPU调用的时间。
  27. SIGPROF:包括进程使用CPU的时间以及系统调用的时间。
  28. SIGWINCH:窗口大小改变时。
  29. SIGIO:文件描述符准备就绪,表示可以进行输入输出操作。
  30. SIGPWR:电源失效信号。
  31. SIGSYS:非法的系统调用。

注:9号和19号信号不允许忽略和捕捉,只能执行默认动作,甚至不能将其设置为阻塞

信号产生

kill函数/命令

kill命令产生信号

1
2
kill -SIGKILL pid;
//pid为要kill的进程ID

kill函数

int kill(pid_t pid,int sig)发送信号给指定进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <signal.h>

int kill(pid_t pid, int sig);


//参数
pid:进程ID;
pid> 0:发送信号给指定的进程;
pid=0:发送信号给与调用kill函数进程属于同一进程的所有进程;
pid< -1:取|pid|(绝对值)发给对应进程组;
pid=-1:发送给进程有权限发送的系统中所有进程;

sig:信号;

//返回值
成功:0;

失败:-1,errno;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cstdio>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>



int main (int argc, char *argv[]) {
pid_t pid=fork();
if(pid>0){
printf("parent,pid=%d\n",getpid());
while (1);
}else if (pid==0) {
printf("child pid=%d,ppid=%d\n",getpid(),getppid());
sleep(2);
kill(getppid(), SIGKILL); //杀死进程
}
return 0;
}

其他几个发信号函数

  • int raise(int sig);
  • void abort(void);
alarm函数

概念

  • alarm函数可以设置定时器,在指定seconds后,内核会给当前进程发送SIGALRM信号,进程收到该信号,默认动作终止
  • 每个进程都有且只有唯一个定时器

函数原型

unsigned int alarm(unsigned int seconds)设置定时器,在指定seconds后,内核会给当前进程发送SIGALRM信号,进程收到该信号,默认动作终止,使用自然计时法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>

unsigned int alarm(unsigned int seconds);


//参数
seconds:闹钟时长(秒);
//alarm(0)等于取消闹钟;


//返回值
成功:返回0或上次定时剩余的秒数;
返回0就是没有用alarm定时;
无失败;

time命令:查看程序执行时间,实际时间=用户时间+内核时间+等待时间

setitimer函数

函数原型

int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value)设置定时器,精度可达微秒,可以实现周期定时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <sys/time.h>

int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);

//参数
which:指定定时形式;
//选择
ITIMER_REAL:自然时间计时法(计算自然时间,也就是在这之间程序终止,时间一到依然发送信号); ->发送SIGALRM信号
ITIMER_VIRTUAL(用户空间):虚拟时间计时法(只计算进程占用cpu的时间); ->发送SIGTVALRM信号
ITIMER_PROF(用户+内核):运行时间计时法(计算占用cpu及执行系统调用的时间); ->发送SIGPROF信号

new_value:定时秒数;

old_value:传出参数,上次定时剩余时间;


//参数类型结构体
struct itimerval {
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};

it_interval:用来设定两次定时任务之间间隔的时间;
it_value:定时的时长;
两个参数都设置为0,则清0操作也就是取消闹钟;

struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};




//返回值
成功:0;

失败:-1,errno;

信号集操作函数

概念

  • 在Linux中阻塞信号集和未决信号集都是存在于PCB中,并且Linux不允许我们直接在上面操作,但是我们又想对阻塞信号集或未决信号集操作就得需要以下函数
  • 我们可以通过自定义的信号集来跟阻塞信号集进行位运算来影响阻塞信号集的各个信号位的值,阻塞信号集受到影响也将影响未决信号集
信号集设定函数
  • int sigemptyset(sigset_t *set)将某个信号集清0

  • int sigfillset(sigset_t *set)将某个信号集全部置为1

  • int sigaddset(sigset_t *set,int signum)把某一个信号(signum)加入到某个自定义的信号集(set)

  • int sigdelset(sigset_t *set,int signum)把某一个信号(signum)从某个自定义信号集(set)中移出

  • int sigismember(const sigset_t *set,int signum)查看某一个信号(signum)是否在某个自定义信号集(set)中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <signal.h>

typedef unsigned long sigset_t;

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

//参数
set:自定义信号集;

signum:信号;

//返回值
成功:0;
sigismember返回值:
0:set没有signum;
1:set有signum;
-1,errno:失败;

失败:-1,errno;
sigprocmask函数

函数原型

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset)用来屏蔽信号和解除屏蔽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <signal.h>

int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);

//参数
how:函数行为设置位,设置函数执行所需要做的行为;
//选择:
SIG_BLOCK:当how设置为此值,set表示需要屏蔽信号.相当于mask=mask|set;
SIG_UNBLOCK:当how设置为此值,set表示需要解除屏蔽的信号.相当于mask=mask&~set;
SIG_SETMASK:当how设置为此值,set表示用于替代原始屏蔽集的新屏蔽集.相当于mask=set,若调用sigprocmask解除了对当前某个若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达;

set:传入参数,自定义信号集,是一个位图,set中哪位置为1,就表示当前进程屏蔽哪个信号;

oldset:传出参数,保存旧的信号屏蔽集,不需要可以传NULL;



//返回值
成功:0;

失败:-1,errno;

注:屏蔽信号,只是将信号处理延后执行(延至解除屏蔽),而忽略表示将信号丢弃处理

sigpending函数

函数原型

int sigpending(sigset_t *set)读取当前进程的未决信号集

1
2
3
4
5
6
7
8
9
10
11
12
#include <signal.h>

int sigpending(sigset_t *set);

//参数
set:传出参数,当前进程的未决信号集;


//返回值
成功:0;

失败:-1,errno;

信号捕捉

概念

  • 信号捕捉使是提供给开发者使用信号捕捉的函数在程序中捕捉到递达到该进程下的信号,然后自定义其的处理方式或者其他操作
signal函数

函数原型

sighandler_t signal(int signum,sighandler_t handler)注册一个信号捕捉函数,设定指定信号需要被捕捉,注册完后,由内核捕捉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);


//参数
signum:信号;

handler:捕捉后行为参数,是一个函数指针也就是sighandler_t定义的函数;

//返回值
成功:返回指向前一个此信号的处理(回调)函数的指针;

失败:返回SIG_ERR;

注:该函数由ANSI定义,在不同版本的Linux中可能有不同的行为,因此我们推荐使用sigaction函数

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <csignal>
#include <cstdio>

//捕捉后行为函数
void sig_cath(int signo){
printf("catch you! %d\n",signo);
return;
}

int main (int argc, char *argv[]) {
signal(SIGINT,sig_cath);
while (1) {
}

return 0;
}

//./test运行
./test
^Ccatch you! 2
^Ccatch you! 2
^Ccatch you! 2
^Ccatch you! 2
^Ccatch you! 2
^Ccatch you! 2
^\zsh: quit (core dumped) ./test

验证signal返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <stdio.h>
#include <signal.h>
using namespace std;

void fun1(int){
cout<<"fun1"<<endl;
}

void fun2(int){
cout<<"fun2"<<endl;
}

int main()
{
void (*res)(int);
if( (res=signal(SIGINT,fun1)) == SIG_ERR ){
perror("error!");
return -1;
}
raise(SIGINT);// == ctrl+c

if( (res=signal(SIGINT,fun2)) == SIG_ERR ){
perror("error!");
return -1;
}
raise(SIGINT);// == ctrl+c
res(SIGINT);//signal返回 指向前一个处理SIGINT的函数 的指针,也就是指向fun1

return 0;
}

//如果按照man手册所说,我们两次raise,一次raise打印fun1,一次raise打印fun2,则此时的返回值res会是fun1的函数指针,因此调用res就会打印fun1
//事实确实如此,运行结果如下:
> ./test
fun1
fun2
fun1
sigaction函数

函数原型

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)用来注册一个信号的捕捉函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <signal.h>


struct sigaction {
void (*sa_handler)(int); //捕捉后行为函数
void (*sa_sigaction)(int, siginfo_t *, void *); //一般不用,要想信号携带复杂数据或结构替数据需要用这个函数
sigset_t sa_mask; //只作用于信号捕捉函数执行期间,当我们执行信号捕捉函数时,如果相同的信号递达,则有可能产生异常,因此sa_mask可以设定进行屏蔽
int sa_flags; //传0,则默认本信号默认屏蔽,则sa_mask本信号位置为1,传1则不屏蔽。
void (*sa_restorer)(void); //废弃了
};



int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);


//参数
signum:要捕捉的信号编号;

act:传入参数,指定新的信号处理方式;

oldact:传出参数,输出上一次的信号处理方式(不为0的话),不需要可以传入NULL;




//返回值
成功:0;

失败:-1,errno;

特性

  • 进程正常运行时,默认PCB中有一个信号屏蔽字mask,它决定了进程自动屏蔽哪些信号,当注册了某个信号捕捉函数,捕捉到该信号以后,要调用信号捕捉函数,而信号捕捉函数有可能执行很长时间,在这期间所屏蔽的信号不由mask来指定,而是用act中的sa_mask来指定,调用完信号处理函数后,再恢复为mask
  • 当sa_flag=0时,xxx信号捕捉函数执行期间,xxx信号会自动被该进程屏蔽
  • 阻塞的常规信号不支持排队,多次产生只记录一次(也就是只处理一次)。(后32个实时信号支持排队)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <csignal>
#include <cstdio>
#include <cstdlib>


void sig_catch(int signo){
if(signo==SIGINT)
printf("catch you!! %d\n",signo);
else if(signo==SIGQUIT)
printf("----catch you!! %d\n",signo);

return;
}

int main (int argc, char *argv[]) {
struct sigaction act,oldact;
act.sa_handler=sig_catch; //设置回调函数
sigemptyset(&act.sa_mask); //将sa_mask屏蔽字清0,在sig_catch工作时有效
act.sa_flags=0; //默认处理动作

int ret=sigaction(SIGINT,&act,&oldact); //注册信号捕捉函数
if(ret==-1){
perror("sigaction error");
exit(1);
}
ret=sigaction(SIGQUIT,&act,&oldact); //注册信号捕捉函数
if(ret==-1){
perror("sigaction error");
exit(1);
}

while(1);
return 0;
}

验证返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <stdio.h>
#include <csignal>
using namespace std;

void fun1(int signo){
cout<<"fun1"<<endl;
}

void fun2(int signo){
cout<<"fun2"<<endl;
}

int main()
{
struct sigaction act,oldact;
act.sa_handler=fun1;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
int ret=sigaction(SIGINT,&act,&oldact);
if (ret==-1){
perror("sigaction error!");
exit(1);
}
raise(SIGINT);// == ctrl+c
act.sa_handler=fun2;
sigemptyset(&act.sa_mask);
ret=sigaction(SIGINT,&act,&oldact);
if(ret==-1){
perror("error!");
return -1;
}
raise(SIGINT);// == ctrl+c
oldact.sa_handler(SIGINT); //sigaction传出参数oldact的sa_handler指向前一个处理SIGINT的函数的指针,也就是指向fun1

return 0;
}

//跟signal结果一致
> ./test
fun1
fun2
fun1
内核实现信号捕捉过程

image-20240501211733359

子进程回收

SIGCHLD信号

产生条件

  • 当子进程状态发生变化,就会产生SIGCHLD信号(例如子进程终止)
借助SIGCHLD信号回收子进程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <bits/types/sigset_t.h>
#include <cstdlib>
#include <unistd.h>
#include <cstdio>
#include <sys/wait.h>
#include <signal.h>

void catch_child(int signo){
pid_t wpid;
int status;
// while((wpid=wait(NULL))!=-1){
//循环回收多个子进程,防止多个子进程一起终止导致sa_mask屏蔽只回收一个,出现僵尸进程
while((wpid=waitpid(-1,&status,0))!=-1){
if(WIFEXITED(status)){
printf("----------catch child id %d,ret=%d\n",wpid,WEXITSTATUS(status));
}
}
return;
}


int main (int argc, char *argv[]) {
pid_t pid;
//使用阻塞屏蔽字来避免注册信号捕捉函数之前子进程终止
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask,SIGCHLD);
sigprocmask(SIG_BLOCK,&mask,NULL);
int i;
for(i=0;i<5;++i){
if ((pid=fork())==0) {
break;
}
}
if(5==i){
struct sigaction act;
act.sa_handler=catch_child;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGCHLD,&act,NULL);
//解除阻塞
sigprocmask(SIG_UNBLOCK,&mask,NULL);
printf("I'm parent,pid=%d\n",getpid());
/* while (1) {

} */
}else {
printf("I'm child pid=%d\n",getpid());
}
return 0;
}

中断系统调用

概念

  • 系统调用分为两类:慢速系统调用和其他系统调用
  • 慢速系统调用可能会使进程永远阻塞的一类系统调用,如果在阻塞期间收到一个信号,该系统调用就被中断,不再继续执行。这一类系统调用有read、write、pause、wait…

慢速系统调用中断问题

  • 慢速系统调用被信号中断后将不会再被执行,而我们可以利用注册信号捕捉函数的sa_flags参数来设置被信号中断后系统调用是否重启。
  • 不重启(默认)SA_INTERRURT重启SA_RESTART

扩展

  • sa_flags还有很多可选参数,如:捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可以将sa_flags设置为SA_NODEFER,除非sa_mask中包含该信号
  • 如果想要信号传递复杂信息(例如结构体),需要将sa_flags设置为SA_SIGINFO,然后使用void (*sa_sigaction)(int, siginfo_t *, void *); 这个函数来进行传递

进程组和会话

进程组

概念

  • 进程组,也称之为作业,代表一个或多个进程的集合,每个进程都有属于的一个进程组,是为了简化对多个进程的管理
  • 父进程创建子进程的时候,默认子进程与父进程属于同一进程组。**进程组ID(PGID)==第一个进程ID(组长进程)**。所以组长进程标识,其进程组ID==其进程ID
  • 可以使用kill -SIGKILL -进程组ID(取负值)来将整个进程组内的进程全部杀死
  • 一个进程可以为自己或子进程设置进程组ID

会话

概念

  • 在Linux中,会话(Session)是指用户与操作系统交互的一段时间
  • 我们常见的Linux session一般是指shell session。Shell session 是终端中当前的状态,在终端中只能有一个 session。当我们打开一个新的终端时,总会创建一个新的 shell session。这表明会话是我们和shell交互的一个过程。
  • tty:文字终端,只能输入命令无图形化
  • pts:虚拟终端,有图形化界面

创建会话注意事项

  1. 调用进程不能是进程组组长,该进程变成新会话首进程
  2. 该进程成为一个新进程组的组长进程
  3. 需要root权限(ubuntu不需要)
  4. 新会话丢弃原有的控制终端,该会话没有控制终端
  5. 该调用进程是组长进程,则出错返回
  6. 建立新会话时,先调用fork,父进程终止,子进程调用setsid()-创建新会话

getsid函数

函数原型

pid_t getsid(pid_t pid)获取进程所属的会话ID

1
2
3
4
5
6
7
8
9
10
11
12
#include <unistd.h>

pid_t getsid(pid_t pid);

//参数
pid:进程ID;
//pid=0表示查看当前进程session ID

//返回值
成功:返回调用进程的会话ID;

失败:-1.errno;

setsid函数

函数原型

pid_t setsid(void)创建一个会话,并以自己的进程ID设置进程组ID,同时也是新会话的ID

1
2
3
4
5
6
7
8
#include <unistd.h>

pid_t setsid(void);

//返回值
成功:返回调用进程的会话ID;

失败:-1,errno;

注:

  • 调用了setsid函数的进程,既是新的会长,也是新的组长
  • 调用进程不能是组长进程

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <unistd.h>
#include <cstdio>
#include <cstdlib>


int main (int argc, char *argv[]) {
pid_t pid;
if ((pid=fork())<0) {
perror("fork error");
exit(1);
}else if (pid==0) {
printf("child process PID is %d\n",getpid());
printf("Group ID of child is %d\n",getpgid(0));
printf("Session ID of child is %d\n",getsid(0));
sleep(10);
setsid(); //子进程非组长进程,故其成为新会话首进程,且成为组长进程,该进程组id即为会话进程
printf("Changed:\n");
printf("child process PID is %d\n",getpid());
printf("Group ID of child is %d\n",getpgid(0));
printf("Session ID of child is %d\n",getsid(0));
sleep(20);
exit(0);
}
return 0;
}

//示例输出,每个人的输出结果都不一样
./test
child process PID is 18261
Group ID of child is 18260
Session ID of child is 16952

~/test
> Changed:
child process PID is 18261
Group ID of child is 18261
Session ID of child is 18261

守护进程

概念

  • 守护进程:叫做Daemon(精灵)进程是Linux中的后台服务进程,通常独立于(脱离)控制终端并且周期性地执行某种人物或等待处理某些发生的事情,一般采用以d结尾的名字
  • Linux后台的一些系统服务进程,没有控制终端,不能直接和用户交互,不受用户登陆、注销的影响,一直在运行着,他们都是守护进程。如:预读入缓输出机制的实现、ftp服务器、nfs服务器等
  • 创建守护进程,最关键一步是调用setsid函数创建一个新的Session,并成为Session Leader

创建守护进程模型

  1. 创建子进程,父进程退出。所有工作在子进程中进行形式上脱离了控制终端
  2. 在子进程中创建新会话,使用setsid()函数,使子进程完全独立出来,脱离控制
  3. **根据需要,改变守护进程工作目录,使用chdir()函数,防止占用可卸载的文件系统(例如U盘)**,防止放在可卸载的文件系统,使得守护进程服务无法执行
  4. 重设文件权限掩码,使用umask()函数,防止继承的文件创建屏蔽字拒绝某些权限,增加守护进程灵活性
  5. **关闭或重定向文件描述符,继承的打开文件不会用到(针对012默认文件描述符)**,重定向的话将012重定向到/dev/null这个空洞文件下,浪费系统资源,无法卸载
  6. 开始执行守护进程核心工作守护进程退出处理程序模型

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <cstdlib>
#include <unistd.h>
#include <cstdio>
#include <sys/stat.h>
#include <fcntl.h>

int main (int argc, char *argv[]) {
pid_t pid;
pid=fork();
//父进程终止
if (pid>0) {
exit(0);
}
//创建新会话
pid=setsid();
if(pid==-1){
perror("setsid error");
exit(1);
}
//改变工作目录位置
int ret=chdir("/home/moon/test");
if(ret==-1){
perror("chdir error");
exit(1);
}
umask(0022); //改变文件访问权限掩码
close(STDIN_FILENO); //关闭文件描述符0
int fd=open("/dev/null",O_RDWR); //fd -->0
if(fd==-1){
perror("open error");
exit(1);
}
dup2(fd, STDOUT_FILENO); //重定向 stdout和stderr
dup2(fd, STDERR_FILENO);
//模拟守护进程业务
while (1) {

}
return 0;
}

//运行
./test
//直接结束,是因为放到后台运行了,使用ps aux或者ps ajx查看

//终止命令
kill -9 test的进程ID

线程控制

线程概念

概念

  • 线程可以看作是轻量级进程(LWP),有独立的PCB,没有独立的进程地址空间(共享)

  • 在Linux下:

    线程:最小的执行单位

    进程:最小分配资源单位,可看成是只有一个线程的进程

  • ps -Lf 进程ID:**可以查看线程号(LWP)**,CPU执行的最小单位

  • 轻量级进程(LWP),也有PCB,创建线程使用的底层函数和进程一样,都是克隆clone

  • 从内核里看进程和线程是一样的,都有各自不同的PCB,但是PCB中指向内存资源的三级页表是相同的

  • 进程可以蜕变成线程

  • 线程可看作寄存器和栈的集合

注:线程ID和线程号是两个东西

  • 线程ID:用来标识线程的
  • 线程号:标识线程身份交给CPU使用分配执行时间

线程共享资源

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间(.text/.data/.bss/heap/共享库)

线程非共享资源

  1. 线程ID
  2. 处理器现场(寄存器值)和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

线程优缺点

  • 优点:

    1. 提高程序并发性
    2. 开销小
    3. 数据通信、共享数据方便
  • 缺点:

    1. 库函数,不稳定
    2. 调试、编写困难、gdb不支持
    3. 对信号支持不好

注:优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大

线程控制原语

  • 在Linux环境中,所有线程特点,失败均直接返回错误号!
  • 所以得到的ret就是错误号,需要通过strerror来对错误号进行解读,说明错误原因

pthread_self函数

函数原型

pthread_t pthread_self(void)获取线程ID,其作用对应进程中getpid()函数

1
2
3
4
5
6
7
8
9
#include <pthread.h>

//pthread_t 类型,在Linux下本质是无符号整数(%lu)
typedef unsigned long int pthread_t;

pthread_t pthread_self(void);

//返回值
成功:获取当前线程ID;

pthread_create函数

函数原型

int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg)创建一个新线程,其作用对应进程中fork()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <pthread.h>

//pthread_t 类型,在Linux下本质是无符号整数(%lu)
typedef unsigned long int pthread_t;


typedef struct{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;



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


//参数
thread:传出参数,新创建的子线程ID;

attr:线程属性参数,通常传NULL,表示使用线程默认属性;

start_routine:函数指针,子线程回调函数。创建成功,pthread_create函数返回时,该函数会被自动调用;

arg:线程主函数执行期间所使用的参数(参数3函数的参数,没有传NULL);



//返回值
成功:0;

失败:错误号;

设置线程属性

  • pthread_attr_t:线程属性结构体
1
2
3
4
5
6
7
8
9
10
11
typedef struct{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
  • int pthread_attr_init(pthread_attr_t *attr)初始化线程属性结构体
1
2
3
4
5
6
7
8
9
10
11
#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);

//参数
attr:传出参数,要初始化的线程属性结构体;

//返回值
成功:0;

失败:错误号;
  • int pthread_attr_destroy(pthread_attr_t *attr)销毁线程属性所占用的资源
1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>

int pthread_attr_destroy(pthread_attr_t *attr);

//参数
attr:要销毁的线程属性结构体;


//返回值
成功:0;

失败:错误号;
  • int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate)设置线程属性是分离还是非分离状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);


//参数
attr:线程属性结构体;

detachstate:分离状态;
//取值:
PTHREAD_CREATE_DETACHED:分离状态;
PTHREAD_CREATE_JOINABLE:非分离状态;


//返回值
成功:0;

失败:错误号;

pthread_create()示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <cstdlib>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <string.h>

//子线程函数
void *tfn(void *arg){
printf("thread:pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}


int main (int argc, char *argv[]) {
//主线程
pthread_t tid;
printf("main:pid=%d,tid=%lu\n",getpid(),tid);
int ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
sleep(1);
return 0;
}

注:因为线程是不共享errno变量,所以直接返回的就是errno,所以得到的ret就是errno编号,需要通过strerror来对errno说明错误原因

创建多个子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cstdlib>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <string.h>

void *tfn(void *arg){
int i=*(int*)arg;
sleep(i);
printf("--I'm %dth thread:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
return NULL;
}


int main (int argc, char *argv[]) {
int i,ret;
pthread_t tid;
for(i=0;i<5;++i){
//因为线程共享i变量,而线程创建和启动可能会有延迟,可能会直到i改变了才开始运行,所以得定义一个局部变量,但是也有另一个方法
int *arg=new int(i);
ret=pthread_create(&tid, NULL,tfn,arg);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
}
sleep(i);
printf("main:I'm Main,pid=%d,tid=%lu\n",getpid(),pthread_self());
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cstdlib>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <string.h>

void *tfn(void *arg){
int i=*(int*)arg;
// sleep(i);
printf("--I'm %dth thread:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
return NULL;
}


int main (int argc, char *argv[]) {
int i,ret;
pthread_t tid;
for(i=0;i<5;++i){
ret=pthread_create(&tid, NULL,tfn,(void *)&i);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
sleep(i);
}
sleep(1);
printf("main:I'm Main,pid=%d,tid=%lu\n",getpid(),pthread_self());
return 0;
}

设置线程属性示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>



void *tfn(void *arg){
printf("thread:pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t tid;
pthread_attr_t attr;
int ret=pthread_attr_init(&attr);
if(ret!=0){
fprintf(stderr,"pthread_attr_init error:%s\n",strerror(ret));
exit(1);
}
//设置线程分离
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//设置线程非分离
// pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_JOINABLE);
if(ret!=0){
fprintf(stderr,"pthread_attr_setdetachstate error:%s\n",strerror(ret));
exit(1);
}
ret=pthread_create(&tid,&attr,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
ret=pthread_attr_destroy(&attr);
if(ret!=0){
fprintf(stderr,"pthread_attr_destroy error:%s\n",strerror(ret));
exit(1);
}

ret=pthread_join(tid,NULL);
if(ret!=0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());

pthread_exit((void *)0);
}

pthread_exit函数

函数原型

void pthread_exit(void *retval)退出当前线程

1
2
3
4
5
6
#include <pthread.h>

void pthread_exit(void *retval);

//参数
retval:表示线程退出状态(退出值),无退出值,传NULL;

注:

  • exit函数是退出进程的函数,所以不能使用exit退出线程,要不然会将这个进程杀死导致其他线程也被删除!
  • return是返回到调用者,也不是退出的意思,但是你也可以返回
  • pthread_exit是将调用该函数的线程退出

pthread_join函数

函数原型

int pthread_join(pthread_t thread,void **retval)阻塞等待线程退出(回收指定线程),获取线程退出状态。其作用对应进程中waitpid()函数,线程不回收跟进程一样会产生僵尸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <pthread.h>

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


//参数
thread:线程ID;

retval:传出参数,存储线程结束状态。因为线程结束回调函数返回值为void*,所以接受返回值就需要void**;



//返回值
成功:0;

失败:错误号;

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <cstdio>


struct thrd{
int var;
char str[256];
};

void *tfn(void *arg){
struct thrd *tval;
tval=(thrd *)malloc(sizeof(struct thrd));
tval->var=100;
strcpy(tval->str,"hello thread");
return (void *)tval;
}

int main (int argc, char *argv[]) {
pthread_t tid;
struct thrd *retval;
int ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
ret=pthread_join(tid,(void **)&retval);
if(ret!=0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);
pthread_exit(NULL);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <cstdio>


struct thrd{
int var;
char str[256];
};

void *tfn(void *arg){
/* struct thrd *tval;
tval=(thrd *)malloc(sizeof(struct thrd)); */
struct thrd* tval=(thrd *)arg;
tval->var=100;
strcpy(tval->str,"hello thread");
//返回的是主线程的arg地址,所以我在回收后直接使用arg也可以的到返回值
return (void *)tval;
}

int main (int argc, char *argv[]) {
pthread_t tid;
struct thrd arg;
struct thrd *retval;
int ret=pthread_create(&tid,NULL,tfn,(void *)&arg);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
ret=pthread_join(tid,(void **)&retval);
if(ret!=0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
printf("child thread exit with var=%d,str=%s\n",retval->var,retval->str);
printf("child thread exit with var=%d,str=%s\n",arg.var,arg.str);
pthread_exit(NULL);
return 0;
}

pthread_detach函数

函数原型

int pthread_detach(pthread_t thread)实现线程分离,可以将指定线程分离出线程组自立门户,线程结束后自动释放资源,不会残留资源在内核中

1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>

int pthread_detach(pthread_t thread);

//参数
thread:待分离线程ID;


//返回值
成功:0;

失败:错误号;

注:

  • 线程分离状态:指定该状态,线程主动与主控线程断开关系,线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用!
  • 进程若有该分离机制,将不会产生僵尸进程。
  • 也可以使用pthread_create函数参2(线程属性)来设置线程分离。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <cstdlib>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <string.h>

void *tfn(void *arg){
printf("thread:pid=%d,tid=%lu\n",getpid(),pthread_self());
return NULL;
}


int main (int argc, char *argv[]) {
int i,ret;
pthread_t tid;
ret=pthread_create(&tid, NULL,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
//设置线程分离,线程终止会自动清理pcb,无需回收
ret=pthread_detach(tid);
if(ret!=0){
fprintf(stderr,"pthread_detach error:%s\n",strerror(ret));
exit(1);
}
sleep(1);
ret=pthread_join(tid,NULL);
printf("join ret=%d\n",ret);
if(ret!=0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}

printf("main:I'm Main,pid=%d,tid=%lu\n",getpid(),pthread_self());
pthread_exit((void *)0);
}

//运行
> ./test
thread:pid=12954,tid=139659550131904
join ret=22
pthread_join error:Invalid argument //发现找不到这个线程,说明线程被pthread_detach分离了

pthread_cancel函数

函数原型

int pthread_cancel(pthread_t thread)杀死(取消)线程,对应进程中的kill()函数

1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>

int pthread_cancel(pthread_t thread);

//参数
thread:要杀死(取消)的线程ID;


//返回值
成功:0;

失败:错误号;

注:

  • 杀死(取消)一个线程,并不是立即杀死(取消),而是得等到到达了一个取消点,再进行取消
  • 取消点是线程检查是否被取消,并按请求进行动作的一个位置,通常是一些系统调用的位置,可以用man 7 pthreads 查看具备这些取消点的系统调用列表
  • 被pthread_cancel()杀死的线程,将会返回-1,使用pthread_join()函数回收

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>


void *tfn(void *arg){
while (1) {
printf("thread:pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(1);
}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t tid;
int ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(5);
ret=pthread_cancel(tid); //终止线程
if(ret!=0){
fprintf(stderr,"pthread_cancel error:%s\n",strerror(ret));
exit(1);
}
while (1);


pthread_exit((void *)0);
}


//运行
> ./test
main:pid=17152,tid=133482618086656
thread:pid=17152,tid=133482611214016
thread:pid=17152,tid=133482611214016
thread:pid=17152,tid=133482611214016
thread:pid=17152,tid=133482611214016
thread:pid=17152,tid=133482611214016
//将会打印5次thread就阻塞在主线程的while中则tid线程被终止了

pthread_testcancel函数

函数原型

void pthread_testcancel(void)为pthread_cancel函数自定义添加取消点

1
2
3
#include <pthread.h>

void pthread_testcancel(void);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>


void *tfn(void *arg){
while (1) {
//将以下两句注释则无法取消这个线程因为没有取消点
/* printf("thread:pid=%d,tid=%lu\n",getpid(),pthread_self());
sleep(1); */
pthread_testcancel(); //自定义取消点,添加取消点则线程可以被杀死
}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t tid;
int ret=pthread_create(&tid,NULL,tfn,NULL);
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
printf("main:pid=%d,tid=%lu\n",getpid(),pthread_self());
ret=pthread_cancel(tid); //终止线程
if(ret!=0){
fprintf(stderr,"pthread_cancel error:%s\n",strerror(ret));
exit(1);
}
pthread_exit((void *)0);
}

//如果没有取消点,则将会一直阻塞在子线程的while(1)循环中

线程使用注意事项

  • malloc和mmap申请的内存可以被其他线程释放(因为线程共享内存)
  • 应避免在多线程模型中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  • 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

线程同步

概念

概念

  • 同步:即协同步调,按预定的先后次序运行
  • 线程同步指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能
  • 同步目的:为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制
  • 所有”多个控制流,共同操作一个共享资源”的情况,都需要同步

数据混乱原因

  1. 资源共享
  2. 调度随机(意味着数据访问会出现竞争)
  3. 线程间缺乏必要的同步机制

互斥量(锁)mutex

函数原型

  • pthread_mutex_t lock定义锁(创建锁)

注:pthread_mutex_t类型,其本质是一个结构体,为简化理解,应用时可忽略其实现细节,简单当成整数看待

  • int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr)初始化互斥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr);


//参数
mutex:要初始化的互斥量(锁);

mutexattr:锁属性参数,通常传入NULL,表示默认属性;

//静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


//返回值
成功:0;

失败:错误号;
  • int pthread_mutex_lock(pthread_mutex_t *mutex)加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);


//参数
mutex:互斥量(锁);


//返回值
成功:0;

失败:错误号;
  • int pthread_mutex_unlock(pthread_mutex_t *mutex)解锁
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);


//参数
mutex:互斥量(锁);


//返回值
成功:0;

失败:错误号;
  • int pthread_mutex_trylock(pthread_mutex_t *mutex)尝试加锁(非阻塞),成功就加锁,失败则返回
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex);


//参数
mutex:互斥量(锁);


//返回值
成功:0;

失败:错误号 如:EBUSY;
  • int pthread_mutex_destroy(pthread_mutex_t *mutex)销毁互斥锁
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);


//参数
mutex:互斥量(锁);


//返回值
成功:0;

失败:错误号;

注:

  • 互斥锁mutex变量的取值为0或1
  • 尽量保证锁的粒度,越小越好**(访问贡献数据前加锁,访问结束立即解锁)**

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>


//定义一把互斥锁(需要全局变量)
pthread_mutex_t mutex;


void *tfn(void *arg){
srand(time(NULL));
while (1) {
pthread_mutex_lock(&mutex); //加锁
printf("hello ");
sleep(rand()%3);
printf("world\n");
pthread_mutex_unlock(&mutex); //解锁
sleep(rand()%3);
}
return NULL;
}


int main (int argc, char *argv[]) {
pthread_t tid;
srand(time(NULL));
int ret=pthread_mutex_init(&mutex,NULL);
if(ret!=0){
fprintf(stderr,"pthread_mutex_init error:%s\n",strerror(ret));
exit(1);
}

ret=pthread_create(&tid,NULL,tfn,NULL); //初始化互斥锁,可以认为锁的值为1
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
while (1) {
pthread_mutex_lock(&mutex); //加锁,mutex--,阻塞线程
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex); //解锁,mutex++,唤醒阻塞在锁上的进程
sleep(rand()%3);
}
ret=pthread_join(tid, NULL);
if(ret!=0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
ret=pthread_mutex_destroy(&mutex); //销毁互斥锁
if(ret!=0){
fprintf(stderr,"pthread_mutex_destroy error:%s\n",strerror(ret));
exit(1);
}
return 0;
}

死锁

概念

  • 死锁是一种使用锁不恰当导致的现象,并不是一把锁

产生情况:

  1. 对一个锁反复lock
  2. 两个线程,线程1持有A锁,请求B锁,线程2持有B锁,请求A锁,导致死循环

读写锁rwlock

概念

  • 与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享
  • 以读方式给数据加锁 称之为读锁
  • 以写方式给数据加锁 称之为写锁

特性

  • 锁只有一把
  • 写锁优先级高
  • 写独占,读共享

读写锁使用特性

  1. 读写锁是”写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞
  2. 读写锁是”读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式对其加锁会阻塞
  3. 读写锁是”读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读模式加锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
  4. 读写锁非常适合对于数据结构读的次数大于写的情况

函数原型

  • int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t attr)初始化读写锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <pthread.h>


int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t attr);


//参数
rwlock:读写锁;

attr:读写锁属性参数,默认属性传NULL;

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


//返回值
成功:0;

失败:错误号;
  • int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)读模式加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);


//参数
rwlock:读写锁;


//返回值
成功:0;

失败:错误号;
  • int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)尝试以读模式加锁,成功就加锁,失败则返回
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);


//参数
rwlock:读写锁;


//返回值
成功:0;

失败:错误号;
  • int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)以写模式加锁
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);


//参数
rwlock:读写锁;


//返回值
成功:0;

失败:错误号;
  • int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)尝试以写模式加锁,成功就加锁,失败则返回
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);


//参数
rwlock:读写锁;


//返回值
成功:0;

失败:错误号;
  • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)解锁
1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

//参数
rwlock:读写锁;


//返回值
成功:0;

失败:错误号;
  • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)销毁读写锁属性所占用资源
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);


//参数
rwlock:读写锁;


//返回值
成功:0;

失败:错误号;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>


int counter;
//定义一把读写锁(需要全局变量)
pthread_rwlock_t rwlock;

/* 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源 */
void *th_write(void *arg){
int t;
int i=*(int*)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
t=counter;
usleep(1000);
printf("======write %d: %lu:counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}

void *th_read(void *arg){
int i=*(int *)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("----------read %d: %lu: %d\n",i,pthread_self(),counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t tid[8];
int i;
int ret=pthread_rwlock_init(&rwlock,NULL); //初始化读写锁
if(ret!=0){
fprintf(stderr,"pthread_rwlock_init error:%s\n",strerror(ret));
exit(1);
}
for(i=0;i<3;++i){
// int *arg=new int(i);
// ret=pthread_create(&tid[i],NULL,th_write,(void *)arg); //以写模式加锁
ret=pthread_create(&tid[i],NULL,th_write,(void *)&i); //以写模式加锁
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
}
for(i=0;i<5;++i){
// int *arg=new int(i);
// ret=pthread_create(&tid[i+3],NULL,th_read,(void *)arg); //以读模式加锁
ret=pthread_create(&tid[i+3],NULL,th_read,(void *)&i); //以读模式加锁
if(ret!=0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
}
for(i=0;i<8;++i){
ret=pthread_join(tid[i], NULL);
if(ret!=0){
fprintf(stderr,"pthread_join error:%s\n",strerror(ret));
exit(1);
}
}
ret=pthread_rwlock_destroy(&rwlock); //销毁读写锁
if(ret!=0){
fprintf(stderr,"pthread_rwlock_destroy error:%s\n",strerror(ret));
exit(1);
}
return 0;
}

条件变量cond

概念

  • 条件变量本身不是锁,但是通常需要结合互斥锁来使用

函数原型

  • int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr)初始化条件变量,一般是动态初始化,在函数体用到再初始化,但是也可以使用静态初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>

int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);


//参数
cond:条件变量;

attr:条件变量属性参数,默认属性传NULL;


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

//返回值
成功:0;

失败:错误号;
  • int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex)

    函数作用:

    1. 阻塞等待一个条件cond满足
    2. 释放已掌握的互斥锁mutex(解锁)
    3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁(加锁)

    1、2步是一个原子操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

//参数
cond:条件变量;

mutex:已掌握的互斥锁;


//返回值
成功:0;

失败:错误号;
  • int pthread_cond_timedwait(pthread_cond_t *cond,pthread_cond_mutex_t *mutex,const struct timespec *abstime)定时等待一个条件满足
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_cond_mutex_t *mutex,const struct timespec *abstime);


struct timespec{
time_t tv_sec; //秒
long tv_nsec; //纳秒
};


//参数
cond:条件变量;

mutex:互斥锁;

abstime:定时时长,需要传绝对时间;
绝对时间:这里的绝对时间是从unix元年也就是1970.1.1开始,假设abs_timeout=3就是1970.1.1日的003秒;

//定时1秒示例
time_t cur=time(NULL); //获取当前时间
struct timespec t; //定义timespec结构体变量
t.tv_sec=cur+1; //定时1秒
t.tv_nsec=t.tv_sec+100;
sem_timedwait(&sem,&t);



//返回值
成功:0;

失败:错误号;
  • int pthread_cond_signal(pthread_cond_t *cond)通知函数,一次通知至少一个线程,也可以唤醒多个线程,但是通常用于通知一个线程
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);


//参数
cond:条件变量;


//返回值
成功:0;

失败:错误号;
  • pthread_cond_broadcast()广播函数,一次通知多个线程
1
2
3
4
5
6
7
8
9
10
11
12
#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);

//参数
cond:条件变量;


//返回值
成功:0;

失败:错误号;
  • int pthread_cond_destroy(pthread_cond_t *cond)销毁条件变量属性所占用资源
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);


//参数
cond:条件变量;


//返回值
成功:0;

失败:错误号;

生产者消费者模型

概念

  • 生产者消费者模型:由生产者进行生产数据,生产者生产的数据将会放置到一个公共区域,由消费者从公共区域拿取消费

生产者模型工作流程

  1. 生产数据
  2. 加锁 pthread_mutex_lock
  3. 将数据放置到公共区域
  4. 解锁 pthread_mutex_unlock
  5. 通知阻塞在条件变量上的线程 pthread_cond_signal或pthread_cond_broadcast
  6. 循环生产后续数据

消费者模型工作流程

  1. 创建锁 mutex
  2. 初始化 pthread_mutex_init
  3. 加锁 pthread_mutex_lock
  4. 等待条件满足:pthread_cond_wait
    1. 阻塞等条件变量
    2. 解锁 unlock
    3. 加锁 lock
  5. 访问共享数据
  6. 解锁、释放条件变量,释放锁

实现示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <unistd.h>

void err_thread(int ret,std::string str){
if(ret!=0){
fprintf(stderr, "%s:%s\n",str.c_str(),strerror(ret));
pthread_exit(NULL);
}
}

struct msg {
int num;
struct msg *next;
};

struct msg *head;

//定义并初始化互斥锁
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
//定义并初始化条件变量
pthread_cond_t has_data=PTHREAD_COND_INITIALIZER;

void *produser(void *arg){
while(1){
struct msg *mp=(msg *)malloc(sizeof(struct msg));
mp->num=rand()%1000+1; //模拟生产数据

printf("--produce:%d\n",mp->num);

int ret=pthread_mutex_lock(&mutex); //加锁
if(ret!=0) err_thread(ret,"pthread_mutex_lock error");
//头插法,写公共区域,将数据放置到公共区域
mp->next=head;
head=mp;
ret=pthread_mutex_unlock(&mutex); //解锁
if(ret!=0) err_thread(ret,"pthread_mutex_unlock error");
//通知消费者,唤醒阻塞在条件变量has_data上的线程
ret=pthread_cond_signal(&has_data);
if(ret!=0) err_thread(ret,"pthread_cond_signal error");
sleep(rand()%3);

}
return NULL;
}

void *consumer(void *arg){
while (1) {
int ret=pthread_mutex_lock(&mutex); //加锁
if(ret!=0) err_thread(ret,"pthread_mutex_lock error");
if(head==NULL){
ret=pthread_cond_wait(&has_data,&mutex); //阻塞等待条件变量,返回时会重新加锁
if(ret!=0) err_thread(ret,"pthread_cond_wait error");
}
//消费数据,读数据
//头删法,取数据
struct msg *mp;
mp=head;
head=mp->next;

printf("-----consumer:%d\n",mp->num);
pthread_mutex_unlock(&mutex); //解锁互斥量

if(ret!=0) err_thread(ret,"pthread_mutex_unlock error");
free(mp);
sleep(rand()%3);

}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t pid,cid;
int ret;
srand(time(NULL));
ret=pthread_create(&pid,NULL,produser,NULL); //生产者
if(ret!=0) err_thread(ret,"pthread_create produser error");
ret=pthread_create(&cid,NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");

//回收生产者和消费者线程
ret=pthread_join(pid, NULL);
if(ret!=0) err_thread(ret,"pthread_join produser error");

ret=pthread_join(cid,NULL);
if(ret!=0) err_thread(ret,"pthread_join consumer error");
return 0;
}

多个消费者示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <unistd.h>

void err_thread(int ret,std::string str){
if(ret!=0){
fprintf(stderr, "%s:%s\n",str.c_str(),strerror(ret));
pthread_exit(NULL);
}
}

struct msg {
int num;
struct msg *next;
};

struct msg *head;

//定义并初始化互斥锁
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
//定义并初始化条件变量
pthread_cond_t has_data=PTHREAD_COND_INITIALIZER;

void *produser(void *arg){
while(1){
struct msg *mp=(msg *)malloc(sizeof(struct msg));
mp->num=rand()%1000+1; //模拟生产数据

printf("--produce:%d\n",mp->num);

int ret=pthread_mutex_lock(&mutex); //加锁
if(ret!=0) err_thread(ret,"pthread_mutex_lock error");
//头插法,写公共区域,将数据放置到公共区域
mp->next=head;
head=mp;
ret=pthread_mutex_unlock(&mutex); //解锁
if(ret!=0) err_thread(ret,"pthread_mutex_unlock error");
//通知消费者,唤醒阻塞在条件变量has_data上的线程
ret=pthread_cond_signal(&has_data);
if(ret!=0) err_thread(ret,"pthread_cond_signal error");
sleep(rand()%3);

}
return NULL;
}

void *consumer(void *arg){
while (1) {
int ret=pthread_mutex_lock(&mutex); //加锁
if(ret!=0) err_thread(ret,"pthread_mutex_lock error");
//循环等待,防止虚假唤醒
while(head==NULL){
ret=pthread_cond_wait(&has_data,&mutex); //阻塞等待条件变量,返回时会重新加锁
if(ret!=0) err_thread(ret,"pthread_cond_wait error");
}
//消费数据,读数据
//头删法,取数据
struct msg *mp;
mp=head;
head=mp->next;

pthread_mutex_unlock(&mutex); //解锁互斥量

if(ret!=0) err_thread(ret,"pthread_mutex_unlock error");
printf("-----consumer id:%lu :%d\n",pthread_self(),mp->num);
free(mp);
sleep(rand()%3);

}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t pid,cid;
int ret;
srand(time(NULL));
ret=pthread_create(&pid,NULL,produser,NULL); //生产者
if(ret!=0) err_thread(ret,"pthread_create produser error");
ret=pthread_create(&cid,NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");

ret=pthread_create(&cid,NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");

ret=pthread_create(&cid,NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");

ret=pthread_create(&cid,NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");

//回收生产者和消费者线程
ret=pthread_join(pid, NULL);
if(ret!=0) err_thread(ret,"pthread_join produser error");

pthread_join(cid,NULL);
if(ret!=0) err_thread(ret,"pthread_join consumer error");
return 0;
}

注:

  • 虚假唤醒:因为pthread_cond_wait会进行解锁操作,多个消费者同时阻塞在一个锁上,容易导致一个线程消费数据解锁后,另一线程拿到锁,但是公共区域没有数据所导致的虚假唤醒

信号量

概念

  • 信号量:是一个既能保证同步、数据不混乱,又能提高线程并发,可以用来实现多线程间对共享资源进行共享,相当于初始化为n的互斥量,n值表示可以同时访问共享数据区的线程数
  • 可以应用于线程、进程间同步

函数原型

  • int sem_init(sem_t *sem,int pshared,unsigned int value)初始化信号量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);


//参数
sem:信号量变量;

pshared:指定是否在线程、进程间共享;
//选择
0:表示线程间同步;
1(非0):表示进程间同步;

value:N值,指定同时访问的线程数;

//返回值
成功:0;

失败:-1,errno;
  • int sem_wait(sem_t *sem)相当于加锁,信号量值大于0,则信号量值-1;信号量等于0,则造成阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <semaphore.h>

int sem_wait(sem_t *sem);


//参数
sem:信号量;



//返回值
成功:0;

失败:-1,errno;
  • int sem_trywait(sem_t *sem)尝试加锁,成功则信号量-1,失败则返回,非阻塞的sem_wait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <semaphore.h>

int sem_trywait(sem_t *sem);


//参数
sem:信号量;



//返回值
成功:0;

失败:-1,errno;
  • int sem_timedwait(sem_t *sem,const struct timespec *abs_timeout)设置定时加锁,定时的sem_wait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <semaphore.h>

int sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);


struct timespec{
time_t tv_sec; //秒
long tv_nsec; //纳秒
};


//参数
sem:信号量;

abs_timeout:定时时长,需要传绝对时间;
绝对时间:这里的绝对时间是从unix元年也就是1970.1.1开始,假设abs_timeout=3就是1970.1.1日的003秒;

//定时1秒示例
time_t cur=time(NULL); //获取当前时间
struct timespec t; //定义timespec结构体变量
t.tv_sec=cur+1; //定时1秒
t.tv_nsec=t.tv_sec+100;
sem_timedwait(&sem,&t);



//返回值
成功:0;

失败:-1,errno;
  • int sem_post(sem_t *sem)相当于解锁,当信号量<N,则信号量++;当信号量==N时,则造成阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <semaphore.h>

int sem_post(sem_t *sem);


//参数
sem:信号量;


//返回值
成功:0;

失败:-1,errno;
  • int sem_destroy(sem_t *sem)释放信号量属性所占用资源
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <semaphore.h可以应用于线程、进程间同步>

int sem_destroy(sem_t *sem);


//参数
sem:信号量;


//返回值
成功:0;

失败:-1,errno;

生产者消费者模型

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <semaphore.h>

#define NUM 5

void err_thread(int ret,std::string str){
if(ret!=0){
fprintf(stderr, "%s:%s\n",str.c_str(),strerror(ret));
pthread_exit(NULL);
}
}

int queue[NUM]; //全局数组实现环形队列
sem_t blank_num,prod_num; //共享区空余信号量,生产产品信号量


void *produser(void *arg){
int i=0;
while(1){
int ret=sem_wait(&blank_num); //要生产产品得减少空余信号量
if(ret!=0) err_thread(ret,"sem_wait error");
queue[i]=rand()%1000+1;
printf("--produce:%d\n",queue[i]);
sem_post(&prod_num); //生产成功产品,则产品信号量增加
if(ret!=0) err_thread(ret,"sem_post error");

i=(i+1)%NUM; //借助下标实现环形

sleep(rand()%1);

}
return NULL;
}

void *consumer(void *arg){
int i=0;
while (1) {
int ret=sem_wait(&prod_num); //消费产品,则产品信号量--(产品>0),否则阻塞
if(ret!=0) err_thread(ret,"sem_wait error");
printf("-----consumer id:%lu :%d\n",pthread_self(),queue[i]);
queue[i]=0;
ret=sem_post(&blank_num); //消费了产品,则产品信号量--
if(ret!=0) err_thread(ret,"sem_post error");
i=(i+1)%NUM;
sleep(rand()%3);

}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t pid,cid;
int ret;
srand(time(NULL));
ret=sem_init(&blank_num,0,NUM); //初始化空余信号量为5,线程间共享
if(ret!=0) err_thread(ret,"sem_init blank_num error");
sem_init(&prod_num,0,0); //初始化产品信号量为0
if(ret!=0) err_thread(ret,"sem_init prod_num error");

ret=pthread_create(&pid,NULL,produser,NULL); //生产者
if(ret!=0) err_thread(ret,"pthread_create produser error");
ret=pthread_create(&cid,NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");


//回收生产者和消费者线程
ret=pthread_join(pid, NULL);
if(ret!=0) err_thread(ret,"pthread_join produser error");

ret=pthread_join(cid,NULL);
if(ret!=0) err_thread(ret,"pthread_join consumer error");
return 0;
}

多个消费者单个生产者示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <semaphore.h>

#define NUM 5
#define NUM_CONSUMERS 3 // 设置消费者数量

void err_thread(int ret,std::string str){
if(ret!=0){
fprintf(stderr, "%s:%s\n",str.c_str(),strerror(ret));
pthread_exit(NULL);
}
}

int queue[NUM]; //全局数组实现环形队列
sem_t blank_num,prod_num; //共享区空余信号量,生产产品信号量

void *producer(void *arg){
int i=0;
while(1){
int ret=sem_wait(&blank_num); //要生产产品得减少空余信号量
if(ret!=0) err_thread(ret,"sem_wait error");
queue[i]=rand()%1000+1;
printf("--producer:%d\n",queue[i]);
sem_post(&prod_num); //生产成功产品,则产品信号量增加
if(ret!=0) err_thread(ret,"sem_post error");
i=(i+1)%NUM; //借助下标实现环形
sleep(rand()%1);
}
return NULL;
}

void *consumer(void *arg){
int i=0;
while (1) {
int ret=sem_wait(&prod_num); //消费产品,则产品信号量--(产品>0),否则阻塞
if(ret!=0) err_thread(ret,"sem_wait error");

// 循环等待直到读取到非零值
while(queue[i] == 0) {
ret=sem_post(&prod_num); // 恢复生产者信号量以防止死锁
if(ret!=0) err_thread(ret,"sem_post error");
usleep(100); // 小睡一会儿以防止忙等
ret=sem_wait(&prod_num); // 重新等待
if(ret!=0) err_thread(ret,"sem_wait error");
}

printf("-----consumer id:%lu :%d\n",pthread_self(),queue[i]);
queue[i]=0;
ret=sem_post(&blank_num); //消费了产品,则产品信号量--
if(ret!=0) err_thread(ret,"sem_post error");
i=(i+1)%NUM;
sleep(rand()%3);
}
return NULL;
}

int main (int argc, char *argv[]) {
pthread_t producer_tid;
pthread_t consumer_tids[NUM_CONSUMERS];
int ret;
srand(time(NULL));

ret=sem_init(&blank_num,0,NUM); //初始化空余信号量为5,线程间共享
if(ret!=0) err_thread(ret,"sem_init blank_num error");
sem_init(&prod_num,0,0); //初始化产品信号量为0
if(ret!=0) err_thread(ret,"sem_init prod_num error");

ret=pthread_create(&producer_tid,NULL,producer,NULL); //生产者
if(ret!=0) err_thread(ret,"pthread_create producer error");

for(int i = 0; i < NUM_CONSUMERS; ++i) {
ret=pthread_create(&consumer_tids[i],NULL,consumer,NULL); //消费者
if(ret!=0) err_thread(ret,"pthread_create consumer error");
}

//回收生产者和消费者线程
ret=pthread_join(producer_tid, NULL);
if(ret!=0) err_thread(ret,"pthread_join producer error");

for(int i = 0; i < NUM_CONSUMERS; ++i) {
ret=pthread_join(consumer_tids[i],NULL);
if(ret!=0) err_thread(ret,"pthread_join consumer error");
}
return 0;
}

Linux系统编程
https://moonfordream.github.io/posts/Linux系统编程/
作者
Moon
发布于
2024年5月19日
许可协议