文件与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
应用场景:
- 文件的“读”、“写”使用同一偏移位置。
- 使用
lseek
获取文件大小(返回值接收),int length = lseek(filepath, 0, SEEK_END);
- 使用
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[]);
这两个函数与execl
和execv
相似,但是会在系统的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
waitpid
是wait
的一个变种,它可以指定等待的子进程的进程 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
管道的读写行为
读管道:
- 管道有数据,
read
返回实际读到的字节数。 - 管道无数据:
- 无写端,
read
返回0 (类似读到文件尾) - 有写端,
read
阻塞等待。
- 无写端,
写管道:
- 无读端, 异常终止。(
SIGPIPE
导致的) - 有读端:
- 管道已满,阻塞等待
- 管道未满, 返回写出的字节个数。
例:使用管道实现父子进程间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_READ
、PROT_WRITE
、MAP_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
使用注意事项:
- 用于创建映射区的文件大小为 0,实际指定非0大小创建映射区,出“总线错误”。
- 用于创建映射区的文件大小为 0,实际指定0大小创建映射区, 出“无效参数”。
- 用于创建映射区的文件读写属性为,只读。映射区属性为读、写。 出“无效参数”。
- 创建映射区,需要read权限。当访问权限指定为“共享”
MAP_SHARED
时, mmap的读写权限,应该<=文件的open权限。只写不行。 - 文件描述符fd,在mmap创建映射区完成即可关闭。后续访问文件,用地址访问。
- offset必须是4096的整数倍。(MMU 映射的最小单位4k )
- 对申请的映射区内存,不能越界访问。
- munmap用于释放的地址,必须是mmap申请返回的地址。
- 映射区访问权限为“私有”
MAP_PRIVATE
, 对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上。 - 映射区访问权限为“私有”
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
数据只能一次读取。
信号
阻塞信号集(信号屏蔽字)
本质:位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,在解除屏蔽前,一直处于未决态。
未决信号集
本质:位图。用来记录信号的处理状态。该信号集中的信号表示,已经产生但尚未被处理。
信号名 | 信号值 | 说明 | 默认处理方式 |
---|---|---|---|
SIGHUP | 1 | 当用户退出shell时,由该shell启动的所有进程将收到这个信号 | 终止进程 |
SIGINT | 2 | 当用户按下<Ctrl + c>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
SIGQUIT | 3 | 当用户按下<Ctrl + >组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出信号 | 终止进程 |
SIGBUS | 7 | 非法访问内存地址,包括内存对齐出错 | 终止进程并产生core文件 |
SIGFPE | 8 | 在发生致命的运算错误发出,不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 | 终止进程并产生core文件 |
SIGKILL | 9 | 无条件终止进程,本信号不能被忽略、处理和阻塞 | 终止进程 |
SIGUSR1 | 10 | 用户定义的信号,即程序员可以在程序中定义并使用该信号 | 终止进程 |
SIGSEGV | 11 | 指示进程进行了无效的内存访问 | 终止进程并产生core文件 |
SIGUSR2 | 12 | 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 | 终止进程 |
SIGPIPE | 13 | 向一个没有读端的管道写数据 | 终止进程 |
SIGALRM | 14 | 定时器超时,超时的时间由系统调用alarm设置 | 终止进程 |
SIGTERM | 15 | 程序结束信号,与SIGKILL 不同的是,该信号可以被阻塞和终止,通常用来表示程序正常退出了。执行shell命令kill时,缺省产生这个信号。 | 终止进程 |
SIGCHLD | 17 | 子进程状态发生变化时,父进程会收到这个信号 | 忽略 |
SIGSTOP | 19 | 停止进程的执行。信号不能被忽略、处理和阻塞。 | 暂停进程 |
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
:要设置处理程序的信号编号,如SIGINT
、SIGTERM
等。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
:要处理的信号编号,如SIGINT
、SIGTERM
等。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
信号捕捉特性:
- 捕捉函数执行期间,信号屏蔽字由 mask --> sa_mask , 捕捉函数执行结束。 恢复回mask;
- 捕捉函数执行期间,本信号自动被屏蔽(sa_flags = 0)。其他信号不屏蔽,如需屏蔽则调用sigaddset函数修改;
- 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次!
例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点注意事项:
- 调用进程不能是进程组组长,该进程变成新会话首进程(平民)
- 该进程成为一个新进程组的组长进程
- 需要root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 该调用进程是组长进程,则出错返回
- 建立新会话时,先调用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
守护进程创建步骤:
- fork子进程,让父进程终止。
- 子进程调用
setsid()
创建新会话 - 通常根据需要,改变工作目录位置
chdir()
, 防止目录被卸载。 - 通常根据需要,重设
umask
文件权限掩码,影响新文件的创建权限。 - 通常根据需要,关闭/重定向 文件描述符
- 守护进程 业务逻辑。
例:
#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;
}
线程属性设置分离线程
步骤:
- 初始化线程属性对象:
int pthread_attr_init(pthread_attr_t *attr);
- 设置线程属性为【分离态】:
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
- 借助修改后的线程属性创建分离态的新线程
- 销毁线程属性对象:
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); // 销毁
线程使用注意事项
- 主线程退出其他线程不退出,主线程应该调用
pthread_exit
- 避免僵尸线程
pthread_join
pthread_detach
pthread_create
指定分离属性- 被join线程可能在join函数返回前就释放自己的所有内存资源,所以不应当返回被回收线程栈中的值
malloc
和mmap
申请的内存可以被其他线程释放- 应避免在多线程中调用
fork
,除非立马exec,子线程中只有调用fork
的线程存在,其他线程在子进程中均pthread_exit
- 信号的复杂语义很难和多线程共存,在多线程中避免使用信号机制
线程同步
互斥锁
创建互斥量: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;
}