Linux系统编程

文件与IO

open

用于打开或创建文件,并返回一个文件描述符。需要包含头文件:#include <unistd.h>;其参数二的flags需要包含头文件:#include <fcntl.h>

函数原型:

int open(char *pathname, int flags)

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

参数:

  • pathname:欲打开的文件路径名
  • flags:文件打开方式,例如O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_CREAT(创建新文件)、O_APPEND(附加)、O_TRUNC(文件截断)、O_EXCL(文件不存在)、O_NONBLOCK(非阻塞)……
  • mode:参数3使用的前提,参2指定了O_CREAT。取值8进制数,用来描述新创建文件的访问权限。创建文件最终权限 = mode & ~umask。

返回值:

  • 成功:打开文件所得到对应的文件描述符(整数)
  • 失败:-1,并设置errno

close

用于释放程序中打开的文件描述符,头文件和open一样。

函数原型:

int close(int fd);

truncate/ftruncate

用于修改文件的大小,可以将文件截断为指定的长度,或者扩展文件到指定的长度。当截断文件时,超出指定长度的部分将被删除;当扩展文件时,文件内容可以保持不变,新增的部分将会被填充为零字节或未定义的内容。需要包含头文件<unistd.h>

函数原型:

int truncate(const char *path, off_t length);

int ftruncate(int fd, off_t length);

参数:

  • path:要操作的文件的路径名。
  • length:指定的文件大小,截断文件时表示新的文件大小,扩展文件时表示文件新的大小。
  • fd:要操作的文件描述符。

返回值:

  • 成功:0
  • 失败:-1,并设置error

read

用于从文件描述符中读取数据,需要包含头文件:#include <unistd.h>

函数原型:

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

参数:

  • fd:文件描述符
  • buf:存数据的缓冲区
  • count:缓冲区大小

返回值:

  • 0:读到文件末尾
  • 成功:> 0,读到的字节数
  • 失败:-1,设置errno
  • -1并且 errno = EAGIN 或 EWOULDBLOCK:说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。

write

用于向文件描述符中写入数据,需要包含头文件:#include <unistd.h>

函数原型:

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

参数:

  • fd:文件描述符
  • buf:待写出数据的缓冲区
  • count:数据大小

返回值:

  • 成功:写入的字节数。
  • 失败:-1,设置 errno

例:运用上面四个系统调用实现linux的cp命令

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

using namespace std;

int main(int argc, char *argv[])
{
    char buf[1024];		//缓冲区
    int fd1 = open(argv[1], O_RDONLY);
    if (fd1 == -1)		//错误处理
    {
        perror("open argv1 error");
        exit(1);
    }
    int fd2 = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd2 == -1)		//错误处理
    {
        perror("open argv2 error");
        exit(1);
    }
    int n;
    while ((n = read(fd1, buf, 1024)) > 0)
    {
        if (n == -1)
            break;
        write(fd2, buf, n);
    }
    close(fd1);
    close(fd2);
    return 0;
}

fcntl

fcntl是UNIX系统提供的一个系统调用,用于对文件描述符进行各种操作。它可以用于执行各种文件描述符相关的控制操作,如修改文件状态标志、获取/设置文件描述符属性、锁定文件等。需要包含头文件:#include <fcntl.h>

函数原型:

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

参数:

  • fd:文件描述符,表示要进行操作的文件描述符。
  • cmd:操作命令,可以指定要执行的操作类型,掌握F_GETFL(获取文件状态)、F_SETFL(设置文件状态)。
  • arg:根据不同的命令类型,arg可能是一个整数值,也可能是一个结构体指针。

返回值的含义取决于执行的具体操作。一般情况下,fcntl调用的返回值可以有以下几种情况:

  • 成功执行:如果 fcntl调用成功执行,返回值通常是与操作相关的具体数值。例如,对于获取文件状态标志 (F_GETFL) 操作,返回的是文件状态标志的值。
  • 失败:如果 fcntl调用执行失败,返回值通常是 -1,并且会设置全局变量 errno来指示具体的错误原因。可以使用perror或其他错误处理函数来获取错误信息。

例:

int flags = fcntl(STDIN_FILENO, F_GETFL); 		//获取stdin属性信息  
flags |= O_NONBLOCK;  							//设置非阻塞
int ret = fcntl(STDIN_FILENO, F_SETFL, flags);  //更新stdin属性信息

lseek

用于在文件描述符上设置文件偏移量(file offset)。文件偏移量是用来指示当前读/写操作在文件中的位置的一个指针。需要包含头文件#include<unistd.h>

函数原型:

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

参数:

  • fd:文件描述符
  • offset:偏移量,就是将读写指针从whence指定位置向后偏移offset个单位
  • whence:起始偏移位置,包括SEEK_SET / SEEK_CUR / SEEK_END

返回值:

  • 成功:较起始位置偏移量
  • 失败:-1,并设置errno

应用场景:

  1. 文件的“读”、“写”使用同一偏移位置。
  2. 使用lseek获取文件大小(返回值接收),int length = lseek(filepath, 0, SEEK_END);
  3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作

例:写一个句子到空白文件,然后调整光标位置,读取刚才写那个文件。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <cstring>

using namespace std;

int main()
{
    char msg[] = "It is a test for lseek.\n";
    int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
    if (fd < 0)
    {
        perror("open file error");
        exit(1);
    }
    write(fd, msg, strlen(msg));

    // 修改文件读写指针位置,位于文件开头。
    lseek(fd, 0, SEEK_SET);

    char ch;
    int n;
    while ((n = read(fd, &ch, 1)) > 0)
    {
        if (n < 0)
        {
            perror("read error");
            exit(1);
        }
        write(STDOUT_FILENO, &ch, n);
    }

    close(fd);
    return 0;
}

stat/lstat

用于获取文件的状态信息,比如文件大小、创建时间、修改时间等。如果文件是一个符号链接,stat函数会进行符号穿透,即返回符号链接文件所指向的文件的信息;而lstat函数将返回符号链接文件本身的信息,而不会解引用符号链接。需要包含头文件#include <sys/stat.h>

函数原型:

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

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

参数:

  • path:文件路径
  • buf:(传出参数)存放文件属性,inode结构体指针。

返回值:

  • 成功: 0
  • 失败: -1,并设置errno
struct stat {
    dev_t     st_dev;         // ID of device containing file
    ino_t     st_ino;         // Inode number
    mode_t    st_mode;        // File type and mode
    nlink_t   st_nlink;       // Number of hard links
    uid_t     st_uid;         // User ID of owner
    gid_t     st_gid;         // Group ID of owner
    dev_t     st_rdev;        // Device ID (if special file)
    off_t     st_size;        // Total size, in bytes
    blksize_t st_blksize;     // Block size for filesystem I/O
    blkcnt_t  st_blocks;      // Number of 512B blocks allocated
    struct timespec st_atim;  // Time of last access
    struct timespec st_mtim;  // Time of last modification
    struct timespec st_ctim;  // Time of last status change
};

获取文件大小: buf.st_size

获取文件类型: buf.st_mode

获取文件权限: buf.st_mode

例:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <cstdlib>

using namespace std;

int main()
{
    struct stat buf;
    int ret = stat("./test.txt", &buf);
    if (ret == -1)
    {
        perror("stat error");
        exit(1);
    }
    cout << "file size: " << buf.st_size << endl;
    return 0;
}

检查文件类型的宏函数:

  • S_ISDIR(mode):检查文件是否为目录文件。
  • S_ISREG(mode):检查文件是否为普通文件。
  • S_ISCHR(mode):检查文件是否为字符特殊文件(字符设备)。
  • S_ISBLK(mode):检查文件是否为块特殊文件(块设备)。
  • S_ISLNK(mode):检查文件是否为符号链接。
  • S_ISFIFO(mode):检查文件是否为FIFO(命名管道)。
  • S_ISSOCK(mode):检查文件是否为套接字。

link/unlink

link函数用于创建一个新的硬链接,将oldpath指定的文件链接到newpath指定的路径上。

unlink函数用于删除指定路径的文件。unlink清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放,要等到所有打开文件的进程关闭该文件,系统才会挑时间将该文件释放掉。

需要头文件#include<unistd.h>

函数原型:

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

int unlink(const char *pathname);

返回值:

  • 0:成功
  • -1:失败

opendir/closedir

opendir函数用于打开一个目录并返回一个指向该目录的指针,以便后续对目录进行读取操作。

closedir函数用于关闭通过 opendir函数打开的目录流,并释放与该目录流相关的资源。

需要包含头文件#include<dirent.h>

函数原型:

DIR *opendir(constchar *name);

int closedir(DIR *dir);

返回值:

  • 0:成功关闭目录
  • -1:关闭目录失败

readdir

readdir函数用于读取目录流中的下一个目录条目,并返回一个指向dirent结构体的指针,该结构体包含有关读取的目录条目的信息。需要包含头文件#include<dirent.h>

函数原型:

struct dirent *readdir(DIR *dir);

参数dir是通过opendir函数打开的目录流指针。

函数返回一个指向dirent结构体的指针,该结构体包含有关下一个目录条目的信息。如果到达目录流的末尾或者发生错误,readdir将返回NULL

dirent结构体定义如下:

struct dirent {
    ino_t d_ino;         // inode number
    off_t d_off;         // offset to the next dirent
    unsigned short d_reclen; // length of this record
    unsigned char d_type;    // type of file
    char d_name[256];    // filename
};

例:递归遍历目录打印文件名和大小

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <cstdlib>
#include <dirent.h>
#include <cstring>

using namespace std;

bool isdir(const char *name)
{
    struct stat buf;
    if (stat(name, &buf) == 0)
    {
        cout << name << ": " << buf.st_size << endl;
        if (S_ISDIR(buf.st_mode))
            return true;
        return false;
    }
    else
    {
        perror("get stat error");
        exit(1);
    }
}

void fetchdir(const char *name)
{
    if (!isdir(name))
        return;
    DIR *dir = opendir(name);
    if (dir == NULL)
    {
        perror("open dir error");
        exit(1);
    }
    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL)
    {
        char *filename = entry->d_name;
        if (strcmp(filename, ".") != 0 && strcmp(filename, "..") != 0)
        {
            char fullname[256];
            sprintf(fullname, "%s/%s", name, filename); // 拼接出完整路径
            fetchdir(fullname);
        }
    }
    closedir(dir);
}

int main(int argc, char *argv[])
{
    if (argc == 1)
        fetchdir(".");
    else
    {
        for (int i = 1; i <= argc; i++)
            fetchdir(argv[i]);
    }
    return 0;
}

dup/dup2

dup函数用于复制文件描述符,返回最小的尚未被使用的文件描述符。

dup2函数也用于复制文件描述符,但是可以指定新的文件描述符,将oldfd拷贝给newfd。使用dup2可以实现重定向的功能。

函数原型:int dup(int oldfd);

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

返回值:成功时返回新的文件描述符,新文件描述符和oldfd指向相同内容;失败时返回 -1。

函数原型:int dup2(int oldfd, int newfd);

参数:

  • oldfd:要复制的文件描述符。
  • newfd:指定的新文件描述符。

返回值:成功时返回新的文件描述符(即newfd),失败时返回 -1。

例:

int fd1 = open(file1, O_RDWR);
int fd2 = open(file2, O_RDWR);
int fdret = dup2(fd1, fd2);		//将fd1复制给fd2,最终fd2也指向file1

// 重定向
dup2(fd1, STDOUT_FILENO);		//将fd1复制给标准输出,最终标准输出也指向file1

进程和进程间通信

fork

用于创建一个新的进程,新进程是调用进程(父进程)的副本。新进程称为子进程。需要包含头文件#include <unistd.h>

函数原型:

pid_t fork();

返回值:

  • 在父进程中,fork返回新创建子进程的进程ID。
  • 在子进程中,fork返回 0。
  • 如果出现错误,fork返回-1。

getpid/getppid

getpid()函数用于获取当前进程的进程ID(PID)。getppid()函数用于获取当前进程的父进程的进程 ID(PPID)。需要包含头文件#include <unistd.h>

函数原型:

pid_t getpid(void);

pid_t getppid(void);

例:循环创建多个进程

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

using namespace std;

int main(int argc, char *argv[])
{
    int i;
    for (i = 0; i < 5; i++)
    {
        if (fork() == 0) // 不允许子进程继续创建
            break;
    }
    if (i == 5)
        cout << "I'm parent, my pid is " << getpid() << ", my parent pid is " << getppid() << endl;
    else
        cout << "I'm child" << i + 1 << ", my pid is " << getpid() << ", my parent pid is " << getppid() << endl;

    return 0;
}

exec函数族

exec()函数族是一组在Unix 和类Unix 操作系统上用于执行新程序的系统调用。这些函数会将当前进程替换为一个新的程序,新程序可以是任何可执行文件,包括二进制可执行文件、脚本文件等。在执行成功时,exec() 函数不会返回,因为当前进程已经被替换为新程序。如果发生错误,则会返回 -1,并设置 errno 错误码来指示问题的原因。

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

这个函数接受一个表示可执行文件路径的字符串path,以及一系列以空指针结束的字符串参数。通常用于指定每个参数的名称,注意参数需要包括命令名,并且用NULL标识结束。例如:execl("/bin/ls", "ls", "-l", NULL)

int execv(const char *path, char *const argv[]);

这个函数接受一个表示可执行文件路径的字符串path,以及一个以空指针结束的字符串数组argv。通常用于传递一个已经构建好的参数数组。例如:

char *args[] = {"ls", "-l", NULL}; 
execv("/bin/ls", args);

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

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

这两个函数与execlexecv相似,但是会在系统的PATH环境变量中搜索可执行文件。例如:execlp("ls", "ls", "-l", NULL);execvp("ls", args);

例:通过程序调用ps -aux并把结果保存在文件里。

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

using namespace std;

int main(int argc, char *argv[])
{
    int fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    dup2(fd, STDOUT_FILENO);
    execlp("ps", "ps", "-aux", NULL);
    close(fd);
    return 0;
}

wait

用于等待子进程结束并获取其状态的系统调用。当父进程调用wait时,它会暂停执行,直到一个子进程结束。一旦子进程结束,wait就会返回子进程的进程ID 和退出状态。需要包含头文件#include<sys/wait.h>

函数原型:

pid_t wait(int *status);

参数:

status是一个整型指针,用于存储子进程的退出状态。如果不关心子进程的退出状态,可以将status设为NULL

返回值:

wait返回值是子进程的进程ID。如果出现错误,则返回-1。

Linux还提供了一些非常有用的宏来帮助解析status参数的状态信息,这些宏都定义在<font style="color:rgb(51, 51, 51);">sys/wait.h</font>头文件中。主要有以下几个:

说明
WIFEXITED(status)如果子进程正常结束,它就返回真;否则返回假。
WEXITSTATUS(status)如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
WIFSIGNALED(status)如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
WTERMSIG(status)如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
WIFSTOPPED(status)如果当前子进程被暂停了,则返回真;否则返回假。
WSTOPSIG(status)如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。

waitpid

waitpidwait的一个变种,它可以指定等待的子进程的进程 ID,并且可以提供一些额外的选项参数。需要包含头文件#include<sys/wait.h>

函数原型:

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

参数:

  • pid:指定回收某一个子进程的pid。

    • >0:待回收的子进程pid;

    • -1:任意子进程;

    • 0:同组的子进程

  • status:(传出)回收进程的状态

  • options:WNOHANG指定回收方式为非阻塞

返回值:

  • >0:表示成功回收的子进程pid

  • 0:函数调用时, 参3指定了WNOHANG, 并且没有子进程结束

  • -1:失败,并设置errno

例:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

using namespace std;

int main(int argc, char *argv[])
{
    int i;
    pid_t pid, wpid, target;
    for (i = 0; i < 5; i++)
    {
        pid = fork();
        if (pid == 0)
            break;
        if (i == 2)
            target = pid;
    }
    if (i == 5)
    {
        sleep(5);
        cout << "I'm parent, before waitpid pid = " << target << endl;
        wpid = waitpid(target, NULL, WNOHANG); // 指定一个进程回收,不阻塞
        // wpid = waitpid(target, NULL, 0); // 指定一个进程回收,阻塞

        cout << "I'm parent, wait a child finish: " << wpid << endl;
    }
    else
    {
        sleep(i);
        cout << "I'm " << i + 1 << "th child, my pid is " << getpid() << endl;
    }

    return 0;
}

pipe

用于创建一个匿名管道,这种管道是一种半双工的通信机制,允许一个进程向管道写入数据,另一个进程从管道读取数据。需要包含<unistd.h>头文件。

函数原型:

int pipe(int pipefd[2]);

参数:

pipefd是一个包含两个整数的数组,用于存放新创建管道的读端和写端的文件描述符。pipefd[0]是管道的读端,pipefd[1]是管道的写端。

返回值:

  • 0:成功
  • -1:失败,并设置errno

管道的读写行为

读管道:

  1. 管道有数据,read返回实际读到的字节数。
  2. 管道无数据:
    1. 无写端,read返回0 (类似读到文件尾)
    2. 有写端,read阻塞等待。

写管道:

  1. 无读端, 异常终止。(SIGPIPE导致的)
  2. 有读端:
    1. 管道已满,阻塞等待
    2. 管道未满, 返回写出的字节个数。

例:使用管道实现父子进程间ls | wc -l通信,子进程执行ls命令,父进程执行wc命令。

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1)
    {
        perror("pipe error");
        exit(1);
    }
    pid_t pid = fork();
    if (pid > 0) // 父进程执行wc
    {
        close(fd[1]);
        dup2(fd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
        perror("execlp wc error");
    }
    else if (pid == 0) // 子进程执行ls
    {
        close(fd[0]);
        dup2(fd[1], STDOUT_FILENO);
        execlp("ls", "ls", NULL);
        perror("execlp ls error");
    }
    else
        perror("fork error");

    return 0;
}

mkfifo

用于在Linux系统中创建一个特殊类型的文件,即命名管道(named pipe),可以用于无血缘关系的进程间通信,使用上类似于文件。需要包含头文件<sys/types.h><sys/stat.h>。如果包含了<unistd.h>就不用包含<sys/types.h>

函数原型:

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

参数:

  • pathname是要创建的命名管道的路径。
  • mode是权限掩码,类似于open函数中使用的权限参数。

返回值:

  • 0:成功
  • -1:失败,并设置errno

例:非血缘关系进程,一个写fifo,一个读fifo

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <sys/stat.h>

using namespace std;

int main()
{
    char buf[2048];
    int ret = mkfifo("fifo", 0664);
    if (ret == -1)
    {
        perror("mkfifo error");
        exit(1);
    }
    int fd = open("fifo", O_WRONLY);
    int i = 0;
    while (1)
    {
        sprintf(buf, "hello fifo %d\n", ++i);
        write(fd, buf, strlen(buf));
        sleep(1);
    }
    return 0;
}
#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

int main()
{
    char buf[2048];
    int fd = open("fifo", O_RDONLY);
    while (1)
    {
        int len = read(fd, buf, 2048);
        write(STDOUT_FILENO, buf, len);
        sleep(1);
    }
    return 0;
}

mmap/munmap

用于在内存中映射文件或者其它对象。这个调用允许将一个文件或设备映射到进程的地址空间,使得文件的内容可以直接在内存中进行读写操作,而无需通过标准的文件 I/O 函数来进行。这种内存映射提供了一种高效的机制,可以让进程直接访问文件内容,从而加速读取和写入操作。需包含头文件#include<sys/mman.h>

函数原型:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数:

  • addr:指定映射区的首地址。通常传NULL,表示让系统自动分配
  • length:共享内存映射区的大小。(须小于等于文件的实际大小)
  • prot:共享内存映射区的读写属性,主要有PROT_READPROT_WRITEMAP_ANONYMOUS,多个属性用位或连接:PROT_READ|PROT_WRITE
  • flags:标注共享内存的共享属性。MAP_SHARED,修改会反映到磁盘上。MAP_PRIVATE修改不反映到磁盘上。
  • fd:用于创建共享内存映射区的那个文件的文件描述符。
  • offset:默认0,表示映射文件全部。偏移位置。需是4k的整数倍

返回值:

  • 成功:映射区的首地址。
  • 失败:MAP_FAILED,即(void*(-1)),并设置errno

匿名映射:只能用于血缘关系进程间通信

p = (int *)mmap(NULL, 40, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

munmap用于取消之前通过mmap函数创建的内存映射,将映射的内存区域从进程的地址空间中移除。取消内存映射后,对映射区域的访问将会触发段错误(segmentation fault)。

函数原型:

int munmap(void *addr, size_t length);

参数:

  • addr:指向映射区域起始地址的指针,即mmap的返回值。
  • length:要取消映射的区域长度。

返回值:

  • 成功:0
  • 失败:-1,并设置errno

使用注意事项:

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

例1:父子进程通过mmap创建的映射区通信

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>

using namespace std;

int var = 100;

int main()
{
    const int length = 4;
    int fd = open("temp", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1)
    {
        perror("open error");
        exit(1);
    }
    ftruncate(fd, length);
    int *p = (int *)mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED)
    {
        perror("mmap error");
        exit(1);
    }
    close(fd); // 映射区建立完毕即可关闭文件

    pid_t pid = fork(); // 创建子进程
    if (pid == 0)
    {
        *p = 2000; // 写共享内存
        var = 1000;
        cout << "child, *p = " << *p << ", var = " << var << endl;
    }
    else if (pid > 0)
    {
        sleep(1);
        cout << "parent, *p = " << *p << ", var = " << var << endl;
        wait(NULL);                  // 回收子进程
        int ret = munmap(p, length); // 释放映射区
        if (ret == -1)
            perror("munmap error");
    }
    return 0;
}

例2:无血缘关系进程间用mmap通信

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string>

using namespace std;

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

void sys_err(char *p)
{
    perror(p);
    exit(1);
}

int main()
{
    student stu = {0, "xiaoming", 18};
    int fd = open("temp", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1)
        sys_err("open error");
    int ret = ftruncate(fd, sizeof(stu));
    if (ret == -1)
        sys_err("ftruncate error");
    student *p = (student *)mmap(NULL, sizeof(stu), PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED)
        sys_err("mmap error");
    close(fd);
    while (1)
    {
        stu.id++;
        *p = stu;
        sleep(1);
    }
    return 0;
}
#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <string>
#include <sys/mman.h>
#include <fcntl.h>

using namespace std;

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

int main()
{
    int fd = open("temp", O_RDONLY);
    student *p = (student *)mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
    close(fd);
    while (1)
    {
        sleep(1);
        cout << "id = " << p->id << ", name = " << p->name << ", age = " << p->age << endl;
    }
    return 0;
}

注意:mmap数据可以重复读取,内容被读走之后不会消失。而fifo数据只能一次读取。

信号

阻塞信号集(信号屏蔽字)

本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。

未决信号集

本质:位图。用来记录信号的处理状态。该信号集中的信号表示,已经产生但尚未被处理。

信号名信号值说明默认处理方式
SIGHUP1当用户退出shell时,由该shell启动的所有进程将收到这个信号终止进程
SIGINT2当用户按下<Ctrl + c>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号终止进程
SIGQUIT3当用户按下<Ctrl + >组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出信号终止进程
SIGBUS7非法访问内存地址,包括内存对齐出错终止进程并产生core文件
SIGFPE8在发生致命的运算错误发出,不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误终止进程并产生core文件
SIGKILL9无条件终止进程,本信号不能被忽略、处理和阻塞终止进程
SIGUSR110用户定义的信号,即程序员可以在程序中定义并使用该信号终止进程
SIGSEGV11指示进程进行了无效的内存访问终止进程并产生core文件
SIGUSR212另外一个用户自定义信号,程序员可以在程序中定义并使用该信号终止进程
SIGPIPE13向一个没有读端的管道写数据终止进程
SIGALRM14定时器超时,超时的时间由系统调用alarm设置终止进程
SIGTERM15程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止,通常用来表示程序正常退出了。执行shell命令kill时,缺省产生这个信号。终止进程
SIGCHLD17子进程状态发生变化时,父进程会收到这个信号忽略
SIGSTOP19停止进程的执行。信号不能被忽略、处理和阻塞。暂停进程

kill

用于向指定进程发送信号。需要包含头文件<signal.h>

函数原型:

int kill(pid_t pid, int sig);

参数:

  • pid:

    • > 0:发送信号给指定进程

    • = 0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程

    • < -1:取绝对值,发送信号给该绝对值所对应的进程组的所有组员

    • = -1:发送信号给有权限发送的所有进程。通常,一个进程只能向拥有相同用户ID的进程发送信号,除非进程具有特权(如root权限)。

  • sig:待发送的信号

返回值:

  • 成功:0
  • 失败:-1,并设置errno

alarm

用于设置闹钟定时器,当设置的闹钟时间到达时,内核会向调用进程发送SIGALRM信号。需要包含头文件<unistd.h>

函数原型:

unsigned int alarm(unsigned int seconds);

参数:seconds设置闹钟定时器的秒数。当经过指定秒数后,将向调用进程发送SIGALRM信号。

返回值:如果之前有闹钟定时器设置,返回值是剩余的秒数。如果之前没有设置定时器,返回值是 0。

注意:alarm(0)用于取消闹钟。

setitimer

允许程序员创建周期性的定时器,用于在指定时间间隔后发送信号给进程。setitimer在实现定时器功能时比alarm更为灵活,可以设置周期性定时器以及定时器到期时的处理方式。需要包含头文件<sys/time.h>

函数原型:

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

参数:

  • which:指定定时器类型,可以是ITIMER_REAL(真实时间,发送的信号为SIGALRM)、ITIMER_VIRTUAL(用户态时间,发送的信号为SIGVTALRM)或 ITIMER_PROF(用户态与内核态时间,发送的信号为SIGPROF)。
  • new_value:指向itimerval结构的指针,包含了新的定时器设置。
  • old_value:传出参数,用于存储之前的定时器设置,如果不需要可以传入 NULL

返回值:

  • 成功:0
  • 失败:-1,并设置errno

itimerval结构:

struct itimerval {
    struct timeval it_interval; // 定时器间隔,用于设定两个定时任务之间的间隔时间
    struct timeval it_value;    // 初始定时器值,即第一次定时秒数
};

struct timeval {
    time_t tv_sec;  // 秒
    suseconds_t tv_usec;  // 微秒
};

可以理解为有2个定时器:

  • 一个用于第一个闹钟什么时候触发
  • 一个用于之后间隔多少时间再次触发闹钟

例:使用setitimer定时,向屏幕打印信息

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

using namespace std;

void myfunc(int signo)
{
    cout << "Hello world!" << endl;
}

void sys_err(char *p)
{
    perror(p);
    exit(1);
}

int main()
{
    struct itimerval it, oldit;
    signal(SIGALRM, myfunc);

    it.it_value.tv_sec = 2;
    it.it_value.tv_usec = 0;
    it.it_interval.tv_sec = 5;
    it.it_interval.tv_usec = 0;

    if (setitimer(ITIMER_REAL, &it, &oldit) == -1)
    {
        perror("setitimer error");
        return -1;
    }
    while (1)
        ;
    return 0;
}

信号集操作函数

清空信号集:int sigemptyset(sigset_t *set);

信号集全部置1:int sigfillset(sigset_t *set);

将一个信号添加到集合中:int sigaddset(sigset_t *set, int signum);

将一个信号从集合中移除:int sigdelset(sigset_t *set, int signum);

判断一个信号是否在集合中,在返回1,不在返回0,出错返回-1:int sigismember(const sigset_t *set, int signum);

参数:

  • set:表示要操作的信号集。
  • signum:指定要添加或移除的信号编号。

返回值:

  • 成功:0
  • 失败:-1,并设置errno

sigprocmask

sigprocmask函数用于检查或修改进程的当前信号屏蔽集。这个函数可以用来设置和获取当前的信号屏蔽集,以及修改这个集合。

函数原型:

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

参数:

  • how:指定如何修改当前信号屏蔽集的方式,可以是以下值之一:
    • SIG_BLOCK:将set中的信号添加到当前的信号屏蔽集中(位与)。
    • SIG_UNBLOCK:从当前的信号屏蔽集中移除set中的信号(取反后再位与)。
    • SIG_SETMASK:将当前的信号屏蔽集替换为set中的信号集合。
  • set:指向新的信号屏蔽集的指针。
  • oldset:传出参数,指向存储之前信号屏蔽集的指针,如果不需要保存之前的信号屏蔽集,可以传入NULL

返回值:

  • 成功:0
  • 失败:-1,并设置errno

sigpending

用于获取当前进程未决信号集

函数原型:

int sigpending(sigset_t *set);

参数:

  • set:传出的未决信号集。

返回值:

  • 成功:0
  • 失败:-1,并设置errno

例:屏蔽SIGINT信号,并循环输出未决信号集确认是否真的屏蔽了

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

using namespace std;

void sys_err(const char *p)
{
    perror(p);
    exit(1);
}

int main()
{
    sigset_t set, oldset, pedset;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    int ret = sigprocmask(SIG_BLOCK, &set, &oldset);
    if (ret == -1)
        sys_err("sigprocmask error");

    while (1)
    {
        if (sigpending(&pedset) == -1)
            sys_err("sigpending error");
        for (int i = 1; i <= 32; i++)
        {
            ret = sigismember(&pedset, i);
            if (ret == 1)
                cout << 1;
            else if (ret == 0)
                cout << 0;
            else
                sys_err("sigismember error");
        }
        cout << endl;
        sleep(1);
    }
    return 0;
}

执行效果:按<Ctrl + c>前全是0,按之后2号信号变成1,并通过<Ctrl + >退出程序

signal

用于设置信号处理程序。当特定信号发生时,操作系统会调用预先注册的信号处理程序来处理该信号。需要包含头文件<signal.h>。存在可移植性问题,不同操作系统存在差异,建议使用sigaction函数。

函数原型:

typedef void (*sighandler_t)(int);		// 定义函数指针,int参数为信号
sighandler_t signal(int signum, sighandler_t handler);

参数:

  • signum:要设置处理程序的信号编号,如SIGINTSIGTERM等。
  • handler:指向处理该信号的函数(信号处理程序)的指针。可以是函数指针,也可以是SIG_IGN(忽略该信号)或SIG_DFL(恢复默认处理方式)。

返回值:

  • 返回一个函数指针,指向之前与该信号相关联的处理程序。如果是第一次设置该信号的处理程序,返回默认的信号处理程序。

例:捕捉SIGINT信号,输出一句话

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

using namespace std;

void myfunc(int sig)
{
    cout << "caught me!!! " << sig << endl;
}

int main()
{
    signal(SIGINT, myfunc);
    while (1)
        ;
    return 0;
}

sigaction

和signal功能一样,但更加强大,推荐使用这个。

函数原型:

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

参数:

  • signum:要处理的信号编号,如 SIGINTSIGTERM等。
  • act:指向struct sigaction结构的指针,用于设置新的信号处理方式。
  • oldact:指向struct sigaction结构的指针,用于存储旧的信号处理方式。

struct sigaction结构体:

struct sigaction {
    void (*sa_handler)(int); // 信号处理函数的地址
    void (*sa_sigaction)(int, siginfo_t *, void *); // 用于处理信号的高级函数,不常用
    sigset_t sa_mask; // 在处理该信号时要阻塞的信号集
    int sa_flags; // 用于设置一些标志,如 SA_RESTART,SA_SIGINFO 等
    void (*sa_restorer)(void); // 恢复函数,在一些旧的系统中会用到(废弃)
};

返回值:

  • 成功:0
  • 失败:-1,并设置errno

信号捕捉特性

  1. 捕捉函数执行期间,信号屏蔽字由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask;
  2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flags = 0)。其他信号不屏蔽,如需屏蔽则调用sigaddset函数修改;
  3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!

例1:signal的例子用sigaction实现

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>

using namespace std;

void sys_err(const char *p)
{
    perror(p);
    exit(1);
}

void myfunc(int sig)
{
    if (sig == SIGINT)
        cout << "caught SIGINT!!! " << sig << endl;
    else if (sig == SIGQUIT)
        cout << "caught SIGQUIT!!! " << sig << endl;
}

int main()
{
    struct sigaction act, oldact;
    act.sa_handler = myfunc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    int ret = sigaction(SIGINT, &act, &oldact);
    ret = sigaction(SIGQUIT, &act, &oldact);
    if (ret == -1)
        sys_err("sigaction error");
    while (1)
        ;
    return 0;
}

执行效果:

例2:借助信号捕捉回收子进程

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/time.h>

using namespace std;

void sys_err(const char *p)
{
    perror(p);
    exit(1);
}

void catch_child(int sig)
{
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, 0)) != -1) // 循环回收,防止僵尸进程出现
    {
        int ret;
        if (WIFEXITED(status))
            ret = WEXITSTATUS(status);
        cout << "catch child pid = " << pid << ", ret = " << ret << endl;
    }
}

int main()
{
    int i;
    pid_t pid;
    struct sigaction sig;

    sig.sa_handler = catch_child;
    sigemptyset(&sig.sa_mask);
    sig.sa_flags = 0;
    sigaction(SIGCHLD, &sig, NULL);

    for (i = 0; i < 50; i++)
    {
        if ((pid = fork()) == 0)
            break;
    }
    if (i == 50)
    {
        cout << "I'm parent, pid = " << getpid() << endl;
        while (1) // 模拟后续逻辑
            ;
    }
    else
    {
        cout << "I'm child, pid = " << getpid() << endl;
        return i;
    }
    return 0;
}

进程组/会话

进程组(别名:作业):

  • 多个进程的集合,每个进程都属于一个一个进程组,简化对多个进程的管理,waitpid函数和kill函数的参数中用到
  • 父进程创建子进程的时候默认父子进程属于同一进程组。进程组的ID第一个进程ID(组长进程),组长进程id进程组id,组长进程可以创建一个进程组,创建该进程组中的进程,然后终止。
  • 只要有一个进程存在,进程组就存在,生存期与组长进程是否终止无关
  • kill -SIGKILL -进程组id 杀掉整个进程组
  • 一个进程可以为自己或子进程设置进程组id

会话:多个进程组的集合

创建会话的6点注意事项:

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

getsid/setsid

getsid函数用于获取指定进程的会话 ID(Session ID)。

函数原型:

pid_t getsid(pid_t pid);

参数:

  • pid:要查询的进程的进程 ID。如果pid为 0,则表示获取调用进程的会话 ID。

返回值:

  • 成功:返回指定进程的会话 ID。
  • 失败:返回-1,并设置errno来指示错误。

setsid函数用于创建一个新的会话(Session),并设置调用进程为这个新会话的首进程(Session Leader)。这个函数通常在守护进程(daemon)的启动过程中被使用,以确保守护进程脱离终端,成为一个独立的会话和进程组。

函数原型:

pid_t setsid(void);

返回值:

  • 成功:返回新会话的会话 ID,即新会话的首进程的进程 ID。
  • 失败:返回-1,并设置errno来指示错误。

守护进程

守护进程又叫daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。不受用户登录注销影响。通常采用以d结尾的命名方式。

创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader

守护进程创建步骤:

  1. fork子进程,让父进程终止。
  2. 子进程调用setsid()创建新会话
  3. 通常根据需要,改变工作目录位置chdir(), 防止目录被卸载。
  4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。
  5. 通常根据需要,关闭/重定向 文件描述符
  6. 守护进程 业务逻辑。

例:

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

using namespace std;

void sys_err(const char *p)
{
    perror(p);
    exit(1);
}

int main()
{
    pid_t pid = fork(); // 创建子进程
    if (pid == -1)
        sys_err("fork error");
    else if (pid > 0) // 父进程终止
        return 0;

    // 以下都是子进程逻辑
    pid = setsid();       // 创建新会话
    int ret = chdir("/"); // 改变工作目录位置
    if (ret == -1)
        sys_err("chdir error");

    umask(0022); // 改变文件权限掩码

    close(STDIN_FILENO);                // 关闭文件描述符0
    int fd = open("/dev/null", O_RDWR); // fd --> 0
    if (fd == -1)
        sys_err("open error");

    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

    while (1) // 模拟守护进程业务
        ;

    return 0;
}

线程

进程:有独立的进程地址空间。有独立的pcb。分配资源的最小单位。

线程:有独立的pcb。没有独立的进程地址空间。执行的最小单位。

下面介绍的函数都需要包含头文件<pthread.h>

pthread_self

pthread_self 函数用于获取调用线程的线程ID(Thread ID),即获取当前线程的标识符。每个线程都有一个唯一的线程ID,用于区分不同的线程。pthread_self函数返回的是pthread_t类型的线程ID。需要包含头文件#include<pthread.h>

函数原型:

pthread_t pthread_self(void);

返回值:

  • 返回调用线程的线程 ID,即pthread_t类型的线程 ID。

pthread_create

pthread_create是POSIX线程库中用于创建新线程的函数。它允许程序员在多线程程序中动态创建新的执行线程。需要包含头文件#include<pthread.h>

函数原型:

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

参数:

  • thread:指向pthread_t类型的变量的指针,用于存储新创建线程的线程ID。
  • attr:指向pthread_attr_t类型的线程属性对象的指针,用于设置新线程的属性,通常传入NULL以使用默认属性。
  • start_routine:指向新线程执行的函数的指针,该函数的返回类型是void *,参数是void *
  • arg:传递给start_routine函数的参数。

返回值:

  • 成功:返回0,表示线程成功创建。
  • 失败:返回一个非零的errno,表示创建线程失败。

例:循环创建子线程

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

using namespace std;

int var = 100;

void sys_err(const char *p)
{
    perror(p);
    exit(1);
}

void *func(void *arg)
{
    int64_t i = int64_t(arg); // 强转
    sleep(i);
    cout << "Thread " << i + 1 << ": pid = " << getpid() << " tid = " << pthread_self() << ", var = " << var << endl; // 线程间共享全局变量
    return NULL;
}

int main()
{
    pthread_t pid;
    int i;
    for (i = 0; i < 5; i++)
    {
        int ret = pthread_create(&pid, NULL, func, (void *)i); // 强转,传值
        if (ret != 0)
            sys_err("pthread_create error");
    }
    sleep(i);
    cout << "Main: pid = " << getpid() << ", tid = " << pthread_self() << endl;
    return 0;
}

pthread_exit

用于终止当前线程的执行并返回一个指定的退出状态值。

函数原型:

void pthread_exit(void *value_ptr);

参数:

  • value_ptr:一个指针,用于传递线程的退出状态值。这个值可以被其他线程通过pthread_join函数获取。无退出值时,可用NULL。

区分

  • exit():退出当前进程。
  • return:返回到调用者那里去。
  • pthread_exit():退出当前线程。

由于主线程可能先于子线程结束,所以子线程的输出可能不会打印出来,当时是用主线程sleep等待子线程结束来解决的。现在就可以使用pthread_exit来解决了。方法就是将return 0替换为pthread_exit,只退出当前线程,不会对其他线程造成影响。

pthread_join

用于等待指定的线程终止并获取其退出状态。当调用pthread_join函数时,当前线程会阻塞,直到指定的线程终止。

函数原型:

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

参数:

  • thread:要等待的线程的线程 ID。
  • status:一个指向指针的指针,用于存储线程的退出状态。如果不关心线程的退出状态,可以传入NULL

返回值:

  • 成功:返回0,表示成功等待指定线程的结束。
  • 失败:返回一个非零的错误码,表示等待线程结束失败。

例:回收线程并获取子线程返回值

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

using namespace std;

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

void sys_err(const char *p)
{
    perror(p);
    exit(1);
}

void *func(void *arg)
{
    int64_t i = (int64_t)arg;
    struct thrd *p = (thrd *)malloc(sizeof(thrd));
    p->var = i;
    strcpy(p->str, "Hello thread!!");
    return p;
}

int main()
{
    struct thrd *p;
    pthread_t pid[5];
    for (int i = 0; i < 5; i++) // 循环创建线程
    {
        int ret = pthread_create(&pid[i], NULL, func, (void *)i);
        if (ret != 0)
            sys_err("pthread_create error");
    }

    for (int i = 0; i < 5; i++) // 循环回收线程
    {
        int ret = pthread_join(pid[i], (void **)&p);
        if (ret != 0)
            sys_err("pthread_join error");

        cout << "Thread " << p->var << ": str = " << p->str << endl;
    }

    return 0;
}

pthread_detach

pthread_detach函数是POSIX 线程库中用于将指定线程标记为“分离状态”的函数。分离状态的线程在终止时会自动释放系统资源,无需其他线程调用pthread_join来回收资源。

函数原型:

int pthread_detach(pthread_t thread);

参数:

  • thread:要被标记为分离状态的线程的线程ID。

返回值:

  • 成功:返回 0,表示成功将线程标记为分离状态。
  • 失败:返回一个非零的错误码,表示标记线程为分离状态失败。

例:使用detach分离线程,分离后的线程会自动回收

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

using namespace std;

void sys_err(const char *p, int errorno)
{
    fprintf(stderr, "%s: %s\n", p, strerror(errorno));
    exit(1);
}

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

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, func, NULL);
    if (ret != 0)
        sys_err("pthread_create error", ret);

    ret = pthread_detach(tid);
    if (ret != 0)
        sys_err("pthread_detach error", ret);

    sleep(1); // 确保子线程先输出
    ret = pthread_join(tid, NULL);
    cout << "join ret = " << ret << endl;
    if (ret != 0)
        sys_err("pthread_join error", ret);

    printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    return 0;
}

pthread_cancel

pthread_cancel函数用于向指定的线程发送取消请求,请求线程终止执行。取消请求本身不会立即终止线程,而是设置一个取消标志,线程在适当的时机(进入内核)检查该标志并决定是否终止执行。若是非常凑巧,线程整个执行过程中不进入内核,就无法取消该线程。因此需要和pthread_testcancel()配合使用。

函数原型:

int pthread_cancel(pthread_t thread);

参数:

  • thread:要取消的线程的线程ID。

返回值:

  • 成功:如果成功向线程发送取消请求,返回 0。
  • 失败:如果发送取消请求失败,返回一个非零的错误码。

注意:成功被pthread_cancel()杀死的线程,在使用pthead_join回收返回值时,会返回-1。

例:主线程调用pthread_cancel杀死子线程

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

using namespace std;

void sys_err(const char *p, int errorno)
{
    fprintf(stderr, "%s: %s\n", p, strerror(errorno));
    exit(1);
}

void *func1(void *arg)
{
    cout << "Thread 1 returning\n";
    return (void *)111;
}

void *func2(void *arg)
{
    cout << "Thread 2 exiting\n";
    pthread_exit((void *)222);
}

void *func3(void *arg)
{
    while (1)
    {
        pthread_testcancel(); // 自己添加取消点
    }
    return (void *)333;
}

int main()
{
    pthread_t tid;
    void *tret = NULL;

    pthread_create(&tid, NULL, func1, NULL);
    pthread_join(tid, &tret);
    cout << "Thread 1 exit code = " << (int64_t)tret << endl;

    pthread_create(&tid, NULL, func2, NULL);
    pthread_join(tid, &tret);
    cout << "Thread 2 exit code = " << (int64_t)tret << endl;

    pthread_create(&tid, NULL, func3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &tret);
    cout << "Thread 3 exit code = " << (int64_t)tret << endl;

    return 0;
}

线程属性设置分离线程

步骤:

  1. 初始化线程属性对象:int pthread_attr_init(pthread_attr_t *attr);
  2. 设置线程属性为【分离态】:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
  3. 借助修改后的线程属性创建分离态的新线程
  4. 销毁线程属性对象:int pthread_attr_destroy(pthread_attr_t *attr);

返回值:成功返回0,失败返回errno

例:

pthread_attr_t attr;
pthread_attr_init(&attr);	// 初始化
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);	// 设置分离态属性
pthread_create(&tid, &attr, tfn, NULL); // 创建分离态的线程
pthread_attr_destroy(&attr);	// 销毁

线程使用注意事项

  1. 主线程退出其他线程不退出,主线程应该调用pthread_exit
  2. 避免僵尸线程
    1. pthread_join
    2. pthread_detach
    3. pthread_create指定分离属性
    4. 被join线程可能在join函数返回前就释放自己的所有内存资源,所以不应当返回被回收线程栈中的值
  3. mallocmmap申请的内存可以被其他线程释放
  4. 应避免在多线程中调用fork,除非立马exec,子线程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit
  5. 信号的复杂语义很难和多线程共存,在多线程中避免使用信号机制

线程同步

互斥锁

创建互斥量:pthread_mutex_t mutex;

初始化:int pthread_mutex_init(pthread_mutex_t ***restrict** mutex, const pthread_mutexattr_t *restrict attr);

销毁:int pthread_mutex_destory(pthread_mutex_t *mutex);

上锁:int pthread_mutex_lock(pthread_mutex_t *mutex);

try锁:int pthread_mutex_trylock(pthread_mutex_t *mutex);

解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);

静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock不同的是,pthread_mutex_trylock是非阻塞的,如果无法立即获取到互斥锁,它会立即返回而不是一直等待。5个函数的返回值都是成功返回0,失败返回错误号

例:

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

using namespace std;

pthread_mutex_t mutex;

void sys_err(const char *p, int errorno)
{
    fprintf(stderr, "%s: %s\n", p, strerror(errorno));
    exit(1);
}

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

int main()
{
    pthread_t tid;

    pthread_mutex_init(&mutex, NULL); // 初始化锁

    int ret = pthread_create(&tid, NULL, func, NULL);
    if (ret != 0)
        sys_err("pthread_create error", ret);

    while (1)
    {
        pthread_mutex_lock(&mutex); // 加锁
        cout << "HELLO ";
        sleep(rand() % 3);
        cout << "WORLD\n";
        pthread_mutex_unlock(&mutex); // 解锁
        sleep(rand() % 3);
    }
    pthread_join(tid, NULL);
    pthread_mutex_destroy(&mutex); // 销毁锁
    return 0;
}

读写锁

读写锁(Read-Write Lock)是一种特殊类型的锁,允许多个线程同时读取共享数据,但在写入时需要独占访问。在 POSIX 线程库中,读写锁通过pthread_rwlock_t类型表示,相关的函数用于对读写锁进行初始化、加锁、解锁和销毁。

注意:

  • 锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
  • 读共享,写独占。
  • 写锁优先级高。
  • 相较于互斥量而言,当读线程多的时候,提高访问效率

初始化:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

  • rwlock:指向pthread_rwlock_t类型的指针,表示要初始化的读写锁。
  • attr:指向pthread_rwlockattr_t类型的指针,表示读写锁的属性,通常为NULL

销毁:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

  • rwlock:指向pthread_rwlock_t 类型的指针,表示要销毁的读写锁。

读方式加锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

  • 以读取方式加锁,允许多个线程同时获取读取锁。
  • 如果有线程持有写入锁,则等待这些线程释放后才能获取读取锁。

写方式加锁:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

  • 以写入方式加锁,独占访问共享资源。
  • 在有线程持有读取或写入锁时,等待这些线程释放后获取写入锁。

解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

  • 解锁读写锁,允许其他线程获取锁。
  • 如果解锁时没有线程持有该锁,则行为未定义。

例:

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

using namespace std;

int counter;
pthread_rwlock_t rwlock; // 全局的读写锁

void *func_read(void *arg)
{
    int64_t i = (int64_t)arg;
    while (1)
    {
        pthread_rwlock_rdlock(&rwlock); // 读加锁,共享
        cout << "Thread " << i << ": tid = " << pthread_self() << ", counter = " << counter << endl;
        pthread_rwlock_unlock(&rwlock); // 解锁
        usleep(2000);
    }
    return NULL;
}

void *func_write(void *arg)
{
    int64_t i = (int64_t)arg;
    while (1)
    {
        pthread_rwlock_wrlock(&rwlock); // 写加锁,独占
        usleep(1000);
        int pre = counter;
        cout << "Thread " << i << ": tid = " << pthread_self() << ", counter = " << pre << ", ++counter = " << ++counter << endl;
        pthread_rwlock_unlock(&rwlock); // 解锁
        usleep(10000);
    }
    return NULL;
}

int main()
{
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock, NULL);

    // 创建5个读线程
    for (int i = 0; i < 5; i++)
        pthread_create(&tid[i], NULL, func_read, (void *)i);

    // 创建3个写线程
    for (int i = 5; i < 8; i++)
        pthread_create(&tid[i], NULL, func_write, (void *)i);

    // 回收线程
    for (int i = 0; i < 8; i++)
        pthread_join(tid[i], NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

条件变量

条件变量(Condition Variables)是一种线程同步原语,用于实现线程之间的等待和通知机制。在 POSIX 线程库中,条件变量通过pthread_cond_t类型表示,相关函数用于等待条件满足和通知等操作。

初始化:int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

  • 用于初始化条件变量对象。
  • cond:指向pthread_cond_t类型的指针,表示要初始化的条件变量。
  • attr:指向pthread_condattr_t类型的指针,表示条件变量的属性,通常为NULL

静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁:int pthread_cond_destroy(pthread_cond_t *cond);

  • 用于销毁条件变量对象。
  • cond:指向pthread_cond_t类型的指针,表示要销毁的条件变量。

等待:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

  • 等待条件变量满足。
  • 如果条件变量满足,解除阻塞并给mutex加锁。
  • 如果条件变量不满足,阻塞等待并给mutex解锁,让给其他线程。
  • 在调用pthread_cond_wait函数时,线程会先释放已经持有的互斥锁,并进入等待状态。因此,在等待条件变量之前,必须先给互斥锁mutex加锁。

限时等待:int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

  • 用于等待条件变量cond满足,最长等待至指定的超时时间。
  • 在调用此函数之前,需要先锁定互斥锁mutex,以避免竞态条件。
  • 如果在超时时间内未收到信号,线程会自动重新获取互斥锁,并继续执行。
  • 如果在超时时间内接收到信号,函数会返回,线程将重新获取互斥锁并继续执行。
  • cond:指向pthread_cond_t类型的指针,表示要等待的条件变量。
  • mutex:指向pthread_mutex_t类型的指针,表示与条件变量关联的互斥锁。
  • abstime:指向struct timespec结构体的指针,表示超时时间,即在此时间之前等待条件变量。

通知一个:int pthread_cond_signal(pthread_cond_t *cond);

  • 向等待该条件变量的一个线程发送信号,唤醒其中一个等待线程。
  • 如果没有等待线程,则不执行任何操作。

广播:int pthread_cond_broadcast(pthread_cond_t *cond);

  • 向等待该条件变量的所有线程发送信号,唤醒所有等待线程。

6个函数的返回值都是:成功返回0,失败返回错误号。

pthread_cond_signal():唤醒阻塞在条件变量上的 (至少)一个线程。

pthread_cond_broadcast():唤醒阻塞在条件变量上的所有线程。

例:借助条件变量模拟 生产者-消费者 问题

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

using namespace std;

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

msg *head; // 共享数据,链表形式

pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 静态初始化条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // 静态初始化互斥量

void sys_err(const char *p, int errorno)
{
    fprintf(stderr, "%s: %s\n", p, strerror(errorno));
    pthread_exit(NULL);
}

void *producer(void *arg)
{
    msg *node;
    while (1)
    {
        // 模拟生产一个数据
        node = new msg;
        node->num = rand() % 1000 + 1;
        cout << "Producer produce " << node->num << endl;

        pthread_mutex_lock(&mutex); // 加锁
        node->next = head;          // 写公共区域
        head = node;
        pthread_mutex_unlock(&mutex); // 解锁

        pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量has_data上的线程
        sleep(rand() % 3);
    }
    return NULL;
}

void *consumer(void *arg)
{
    msg *node;
    while (1)
    {
        pthread_mutex_lock(&mutex); // 加锁
        while (head == NULL)
            pthread_cond_wait(&has_data, &mutex); // 等待条件变量
        node = head;
        head = head->next;
        pthread_mutex_unlock(&mutex); // 解锁

        cout << "Consumer consume " << node->num << endl;
        delete node;
        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    int ret;
    pthread_t pro, con;
    srand(time(0));

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

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

    pthread_join(pro, NULL);
    pthread_join(con, NULL);
    return 0;
}

信号量

信号量应用于线程、进程间同步。相当于初始化值为N的互斥量。N值,表示可以同时访问共享数据区的线程数。需要包含头文件<semaphore.h>

定义类型:sem_t sem;

初始化:int sem_init(sem_t *sem, int pshared, unsigned int value);

  • sem:指向要初始化的信号量的指针。
  • pshared:指定信号量是在进程间共享(非零)还是在当前进程内(即线程间)共享(零)。
  • value:信号量的初始值。

销毁:int sem_destroy(sem_t *sem);

等待信号量,并将信号量的值减一。如果信号量的值小于等于 0,则会阻塞直到信号量的值大于0:int sem_wait(sem_t *sem);

增加信号量的值并唤醒等待该信号量的进程:int sem_post(sem_t *sem);

获取信号量当前值:int sem_getvalue(sem_t *sem, int *sval);

  • sem:指向要获取值的信号量的指针。
  • sval:用于存储信号量值的整数指针。

例:信号量实现生产者-消费者

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

using namespace std;

const int NUM = 5;
int queue[NUM];               // 环形队列
sem_t blank_num, product_num; // 空位信号量和产品信号量

void *producer(void *arg)
{
    int i = 0;
    while (1)
    {
        sem_wait(&blank_num);         // 空位减一
        queue[i] = rand() % 1000 + 1; // 生产一个产品
        cout << "Producer produce " << queue[i] << endl;
        sem_post(&product_num); // 产品加一

        i = (i + 1) % NUM; // 实现环形队列
        sleep(rand() % 2);
    }
    return NULL;
}

void *consumer(void *arg)
{
    int i = 0;
    while (1)
    {
        sem_wait(&product_num); // 产品减一
        cout << "--Consumer consume " << queue[i] << endl;
        queue[i] = 0;         // 消费一个产品
        sem_post(&blank_num); // 空位加一

        i = (i + 1) % NUM; // 实现环形队列
        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    sem_init(&blank_num, 0, NUM); // 初始化空位信号量为5
    sem_init(&product_num, 0, 0); // 初始化产品信号量为0

    pthread_t pro, con;
    pthread_create(&pro, NULL, producer, NULL);
    pthread_create(&con, NULL, consumer, NULL);

    pthread_join(pro, NULL);
    pthread_join(con, NULL);

    sem_destroy(&blank_num);
    sem_destroy(&product_num);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

h0l10w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值