1.基础入门

1.1基本函数

open()函数:打开一个文件

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

  • 参数:

    • pathname:想要打开的文件路径名

    • flags(头文件#include ):文件打开方式:O_CREAT|OAPPEND|…

    • mode:权限,当第二个参数设置了O_CREAT,就需要写该参数

mode:设置文件的权限(参数3使用前提:参数2指定了O_CREAT)

  • 返回值:

    • 成功:打开文件所得到对应的文件描述符(整数)

    • 失败:-1,设置errno

read()函数:往一个文件读内容

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

  • 参数:

    • fd:文件描述符

    • buf:存数据的缓冲区

    • count设置读字节数大小

count:缓冲区大小

  • 返回值:

    • 成功:读到的字节数

    • 失败:-1,设置errno

write()函数

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

  • 参数:

    • fd:文件描述符

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

    • count:写入数据字节数大小

  • 返回值:

    • 成功:写入的字节数

    • 失败:-1,设置errno

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//将文件cp一份
int main(int argc,char *argv[]){
char buf[3]; //定义一个buf,用来存放读和写的内容
int n=0;
int fd1 = open(argv[1],O_RDONLY); //只读方式打开文件argv[1]
int fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664); //读写|文件不存在就创建出来|文件存储就覆盖;权限
while((n = read(fd1,buf,3)) != 0){ //一次循环最多读3字节,n是每次读到的字节数
write(fd2,buf,n); //一次循环写入n个字节,也就是上面读到的字节数
}
printf("fd1 = %d\n",fd1); //通过文件描述符打印内容
printf("fd2 = %d\n",fd2); //通过文件描述符打印内容
close(fd1); //关闭文件描述符
close(fd2); //关闭文件描述符
return 0;
}

1.2基本知识

1.PCB进程控制块:本质是结构体

  • 成员:文件描述符表——>文件描述符:0/1/2/3/…/1023

key value(指针:指向一个结构体)

0 – STDIN_FILENO ——–>标准输入

1 – STDOUT_FILENO —–>标准输出

2 – STDERR_FILENO ——>标准错误

注意:每次得到的文件描述符默认是表中可用最小的

2.阻塞、非阻塞

是设备文件、网络文件的属性。产生阻塞的场景:读设备文件、读网络文件(读常规文件无阻塞概念)

dev/tty:终端文件。标准输入、标准输出、标准错误都在终端显示

非阻塞设置:open(“/dev/tty”,..|O_NONBLOCK)

  • 此时若返回-1,并且errno = EAGIN或EWOULDBLOCK,说明不是read失败,而是read在以非阻塞方式读一个设备文件或网络文件,并且文件无数据(默认是阻塞状态)
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
int main(void){
char buf[10]; //定义一个buf,用来存放读和写的内容
int fd,n;
fd = open("/dev/tty",O_RDONLY|O_NONBLOCK); //以只读方式|非阻塞方式 打开文件/dev/tty
if(fd<0){
perror("open /dev/tty"); //如果fd<0,说明打开文件失败,打印错误信息,并退出
exit(1);
}
tryagain:
n=read(fd,buf,10); //从/dev/tty读数据存到buf(最多读10字节),/dev/tty是键盘输入
if(n<0){
if(errno != EAGAIN){ //说明read读取失败,某些异常造成
perror("read /dev/tty");
exit(1);
}else{ //=EAGAIN是阻塞情况下无数据读,输出try again到终端继续等待键盘输入
write(STDOUT_FILENO,"try again\n",strlen("try again\n"));
sleep(2); //2秒钟执行一次
goto tryagain;
}
}
//读到数据情况
write(STDOUT_FILENO,buf,n); //在2秒钟内读到数据就直接输出到终端
close(fd); //关闭文件描述符
return 0;
}

3.参数

  • 传入参数:

    • 指针作为函数参数

    • 有const关键字修饰

    • 指针指向有效区域,在函数内部做读操作

  • 传出参数:

    • 指针作为函数参数

    • 在函数调用之前,指针指向的空间可以无意义,但必须有效

    • 在函数内部,做写操作

    • 函数调用结束后,充当函数返回值

  • 传入传出参数:

    • 指针作为函数参数

    • 在函数调用之前,指针指向的空间有实际意义

    • 在函数内部,先做读操作,后做写操作

    • 函数调用结束后,充当函数返回值

4.文件存储

  • dentry:目录项,其本质是结构体,重要成员变量有两个{文件名,inode,…},而文件内容(data)保存在磁盘块中。

  • inode:其本质是结构体,存储文件的属性信息。如权限、类型、大小、盘块位置…。大多数的inode都存储在磁盘上,少数常用、近期使用的inode会被存储在内存上。

1.3进阶函数

1.fcntl函数

  • 获取文件状态:F_GETFL

  • 设置文件状态:F_SETFL

获取fd描述符的stdin属性信息:int flgs = fcntl(fd,F_GETFL);

添加非阻塞状态:flgs |= O_NONBLOCK;

把设置的状态信息设置到终端设备的状态信息中:fcntl(fd,F_SETFL,flgs);

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
#define MSG_TRY "try again\n"               //定义宏变量
int main(void){
char buf[10];
int flags,n;
flags = fcntl(STDIN_FILENO,F_GETFL); //获取stdin属性信息(获取标准输入对应的终端设备的状态信息) STDIN_FILENO对应终端设备
if(flags == -1){
perror("fcntl error");
exit(1);
}
flags |= O_NONBLOCK; //添加非阻塞状态
int ret = fcntl(STDIN_FILENO,F_SETFL,flags); //把设置的状态信息设置到终端设备的状态信息中
if(ret == -1){
perror("fcntl error");
exit(1);
}
tryagain:
n=read(STDIN_FILENO,buf,10);
if(n<0){
if(errno != EAGAIN){ //说明read读取失败,不是因为没有数据
perror("read /dev/tty");
exit(1);
}else{
sleep(3);
write(STDOUT_FILENO,MSG_TRY,strlen(MSG_TRY));
goto tryagain;
}
}
//读到数据情况
write(STDOUT_FILENO,buf,n);
return 0;
}

2.lseek函数

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

  • 参数:

    • fd: 文件描述符

    • offset:偏移量

    • whence:偏移位置:SEEK_SET(起点)/SEEK_CUR(当前位置)/SEEK_END(终点)

  • 返回值:

    • 成功:较起始位置偏移量

    • 失败:-1 设置errno

应用场景:

  • 文件的读和写使用同一偏移位置(读和写都改变偏移位置)

  • 使用lseek获取文件大小

  • 使用lseek拓展文件大小(要想使文件大小真正拓展,必须引起IO操作)

补:可以使用truncate函数,直接拓展文件大小

  • 如int ret = truncate(“dict.txt”,250);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//测试文件的读和写使用同一偏移位置
int main(void){
int fd,n;
char msg[] = "It's a test4 for lseek\n";
char ch;
fd = open("lseek.txt",O_RDWR|O_CREAT,0644); //以读写方式打开|不存在就创建
if(fd<0){
perror("open lseek.txt error");
exit(1);
}
write(fd,msg,strlen(msg)); //使用fd对打开文件进行写操作,此时读写位置位于文件结尾处

//此时读写指针位于结尾处了,没有下面这行,将只能写数据到文件中,但不能执行下面的读(读出数据)
lseek(fd,0,SEEK_SET); //将读写指针设置到文件开头----->读写指针;偏移量为0;开始偏移的位置为起始位置
while((n = read(fd,&ch,1))){ //读,以一个变量为缓冲区,缓冲区大小为1
if(n < 0){
perror("read error");
exit(1);
}
write(STDOUT_FILENO,&ch,n); //将文件内容按字节读出,写到屏幕上
}
close(fd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
//使用lseek获取文件大小
int main(void){
int fd = open("lseek.txt",O_RDWR); //以读写打开文件
if(fd == -1){
perror("open error");
exit(1);
}
//此时读写指针位于文件开头
int lenth = lseek(fd,0,SEEK_END); //偏移量为0,偏移位置为文件末,所以读写指针从文件头到文件尾,返回偏移了多少
printf("file size:%d\n",lenth);
return 0;
}

3.stat/lstat函数

stat底层是一个结构体,里面有文件的信息

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

  • 参数:

    • path:文件或目录路径

    • buf:(传出参数)存放文件属性

  • 返回值:

    • 成功:0

    • 失败:-1 设置errno

系统提供函数

  • 获取文件大小:buf.st_size

  • 获取文件类型:buf.st_mode

  • 获取文件权限:buf.st_mode

符号穿透:stat会;lstat不会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(){
struct stat sbuf;
int ret = stat("document",&sbuf); //查看的目录文件路径;传出参数(接受该文件的stat结构体)
if(ret == -1){
perror("stat error");
exit(-1);
}
printf("file size:%ld\n",sbuf.st_size); //打印该文件的st_size信息
if(S_ISREG(sbuf.st_mode)){
printf("It's a regular\n"); //这是一个普通文件
}else if(S_ISDIR(sbuf.st_mode)){
printf("It's a dir\n"); //这是一个目录
}else if(S_ISFIFO(sbuf.st_mode)){
printf("It's a pipe\n"); //这是一个管道
}else if(S_ISLNK(sbuf.st_mode)){
printf("It's a sym link\n"); //这是一个软连接
}
return 0;
}

4.link/unlink函数

link函数:可以为已经存在的文件创建目录项(硬链接)

1
2
link("a.txt","b.txt");    //旧文件;新文件
unlink("a.txt"); //删除旧的,新的可以继续用

unlink函数:删除一个文件的目录项。从某种意义上说,只是让文件具备了释放的条件。

unlink函数的特征:清除文件时,如果文件的硬链接数到0了,没有dentry对应,但该文件仍不会马上被释放(只是目录中我们看不到该文件了而已),要等到所有打开该文件的进程关闭该文件,系统才会挑时间将该文件释放掉

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
int main(void){
int fd;
char *p = "test of unlink\n";
char *p2 = "after write something\n";
fd = open("temp.txt",O_RDWR|O_CREAT|O_TRUNC,0644); //打开文件temp.txt,没有就创建出来
if(fd < 0){
perror("open temp error");
exit(1);
}
sleep(5); //这些时间可以看到该文件
int ret = unlink("temp.txt"); //具备了被释放的条件,在目录中我们看不到了
if(ret < 0){
perror("unlink error");
exit(1);
} //虽然unlink了,但后面还能进行写内容
ret = write(fd,p,strlen(p)); //往fd里面写内容
if(ret == -1){
perror("--------write error");
}
printf("hi! I'm lxx\n");
ret = write(fd,p2,strlen(p2)); //往fd里面继续写内容
if(ret == -1){
perror("------Write error");
}
printf("enter anykey continue\n");
getchar();
close(fd);
}

5.readlink()函数

作用:读取符号链接(软链接)文件本身内容,得到链接所指向的文件名。

如有软链接:t.soft -> /home/hoem1/test

终端执行:readlink t.soft 得到:/home/hoem1/test

6.目录操作函数

DIR * opendir(char *name);

int closedir(DIR *dp)

struct dirent *readdir(DIR *dp);

其中:

1
2
3
4
5
struct dirent{
inode
char dname[256]
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc,char *argv[]){
DIR *dp;
struct dirent *sdp; //dirent是目录项类型,相当于dentry
dp = opendir(argv[1]);
if(dp == NULL){
perror("opendir error");
exit(1);
}
while((sdp = readdir(dp)) != NULL){
//去掉目录项文件名是.和..的目录项
if((strcmp(sdp->d_name,".") == 0)||(strcmp(sdp->d_name,"..") == 0))
continue;
printf("%s\t",sdp->d_name); //打印每个目录项的文件名
}
printf("\n");
closedir(dp);
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
void isFile(char *name);    //申明isFile函数
//打开目录读取,处理目录
void read_dir(char *dir){ //是目录,就打开其目录项
char path[256]; //用于后面拼接路径
DIR *dp;
struct dirent *sdp;
dp = opendir(dir);
if(dp == NULL){
perror("opendir error");
return;
}
//读取目录项
while((sdp = readdir(dp)) != NULL){
if(strcmp(sdp->d_name,".") == 0 || strcmp(sdp->d_name,"..") == 0){
continue; ////不排除这个,会进入死循环
}
//目录项本身不可访问,拼接目录
sprintf(path,"%s/%s",dir,sdp->d_name);
//判断文件类型,目录递归进入,文件显示名字/大小
isFile(path);
}
closedir(dp);
return;
}
void isFile(char *name){
int ret = 0;
struct stat sb; //stat是一个结构体,里面包含对应文件的信息
ret = stat(name,&sb); //获取文件属性,判断文件类型
if(ret == -1){
perror("stat error");
return;
}
if(S_ISDIR(sb.st_mode)){ //是否目录
read_dir(name); //是目录,进入read_dir函数,继续处理
}
//是普通文件,打印名字和大小
printf("%s\t%ld\n",name,sb.st_size);
return;
}
int main(int argc, char *argv[]){
if(argc == 1){ //如果没有传入参数,就遍历当前目录下的内容
isFile(".");
}else{
isFile(argv[1]);
}
return 0;
}

7.dup/dup2函数

int dup(int oldfd);

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

  • 返回值:新文件描述符

int dup2(int oldfd,int newfd);

  • 参数:

    • oldfd:原文件描述符

    • newfd:新文件描述符—–>指向源文件描述符所指向的文件

  • 返回值:新文件描述符

1
2
3
4
5
6
7
8
9
10
int main(int argc,char *argv[]){
int fd1 = open(argv[1],O_RDWR); //读写方式打开文件 fd1-->3
int fd2 = open(argv[2],O_RDWR); //读写方式打开文件 fd2-->4
int fdret = dup2(fd1,fd2); //fd2与fd1指向的是同一个内存空间了,返回新文件描述符fd2
printf("fdret = %d\n",fdret);
int ret = write(fd2,"1234567",7); //通过fd2来写内容,内容写在的是第一个文件中
dup2(fd1,STDOUT_FILENO); //令屏幕输入(fd=1)重定向给fd1所指向的文件
printf("--------------------886\n"); //向屏幕输出的内容都写到文件描述符3所在的内存空间
return 0;
}

fcntl函数是实现dup

int fcntl(int fd,int cmd,…);

  • 参数:

    • fd:原文件描述符

    • cmd:F_DUPFD(特定参数)

    • 参数3:指定文件描述符号。被占用的,返回最小可用的;未被占用的,返回=该值的文件描述符

  • 返回值:返回新的文件描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
//fcntl也可以像dup那些使用
int main(int argc,char *argv[]){
int fd1 = open(argv[1],O_RDWR); //读写方式打开文件
printf("fd1 = %d\n",fd1); //打印文件描述符fd1的值,是3
//原文件描述符;特定参数;新的文件描述符号(已经存在的话,系统就按正常给)
int newfd = fcntl(fd1,F_DUPFD,0); //0被占用,fcntl使用文件描述符表中可用的最小文件描述符来返回,即newfd=4
printf("newfd = %d\n",newfd); //newfd的值是4
int newfd2 = fcntl(fd1,F_DUPFD,7); //7未被占用,返回文件描述符7(如果存在7,就返回更大的号)
printf("newfd = %d\n",newfd2);
int ret = write(newfd2,"yyyyyy",6); //写在的是fd1文件描述符所在的文件
printf("ret = %d\n",ret);
return 0;
}

2.进程

2.1进程与程序

程序:死的,只占用磁盘空间 —剧本

进程:活的,运行起来的程序,占用内存、cpu等系统资源 —戏剧

1.进程控制块PCB

我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。

内部成员:

  • 进程id:系统中每个进程有唯一的id,在c语言中用pid_t类型表示,其实就是一个非负整数;

  • 进程的状态:有初试、就绪、运行、挂起和停止五种状态

  • 进程切换时想要保存和恢复的一些CPU寄存器;(了解)

  • 描述虚拟地址空间的信息;(了解)

  • 描述控制终端的信息;(了解)

  • 当前所处工作目录;

  • umask掩码:rwx对应124,当umask为022时,在你创建文件和目录时,默认权限是777-022=755,即rwxr_xr_x;(了解)

  • 文件描述符表:是map结构,key为正整数,value为指针,指针指向结构体;它包含很多指向file结构体的指针(每个进程都有文件描述符表)

  • 和信号相关的信息;

  • 用户id和组id;

  • 会话和进程组;(了解)

  • 进程可用使用的资源上限;(了解)

2.进程共享

父进程在fork()之后:

相同:全局变量、data段、text段、栈、堆、环境变量、用户id、宿主目录、进程工作目录、信号处理方式…

不相同:进程id、fork返回值、父进程id、进程运行时间、闹钟(定时器)、未决信号集

注:对于全局变量,父子进程间遵循读时共享写时复制的原则

3.孤儿进程/僵尸进程

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

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

3.相关函数

3.1fork()函数

功能:是创建一个新的进程

pid_t fork(void);

  • 返回值:对应文件的子进程id号

注意:创建出来的子进程可用执行父进程中fork()函数下面的代码。在父进程中,fork返回的是子进程id号;在子进程中,fork返回的是0;

补:getpid()返回的是当前进程;getppid()返回的是父进程;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//通过fork()函数来生成子进程
int main(int argc,char* argv[]){
pid_t pid = fork(); //生成一个子进程
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){ //子进程会执行该行
printf("---child is created,pid = %d,parent-pid = %d\n",getpid(),getppid());
}else if(pid > 0){ //fork成功的话,父进程里返回的是子线程的id,所以父线程执行该行
printf("---parent process: my child is %d,my pid = %d,my parent pid = %d\n",pid,getpid(),getppid());
}
printf("======================end of file\n");
sleep(3); //防止父进程结束了,子进程还没有打印内容,当父进程结束后,退出程序,而子进程就没有父进程了,打印的父进程就为1
}

在上面代码中,子进程和父进程都会执行sleep(3)这行代码,不是说只有让父进程执行,而子进程不执行,sleep(3)只是防止父进程结束退出程序了,子进程还没有打印父进程。如果父进程结束了,子进程才打印父进程,则父进程是1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//循环创建5个子线程,且有序打印出
int main(int argc,char* argv[]){
int i;
pid_t pid;
for(i = 0;i < 5; i++){
if(fork() == 0){ //子线程创建好了与0做对比,直接跳出循环,但父进程还需要继续执行
break;
}
}
if(i == 5){
sleep(5);
printf("我是父进程\n");
}else{ //当第1个子进程创建好后,此时i=0;当第2个子进程创建好后,此时的i=1;.....
sleep(i);
printf("我是第%d个子线程\n",i+1);
}
return 0;
}

3.2 execlp()函数

功能:指定进程执行相应的程序

int execlp(const *file , const *arg , …);

  • 参数:

    • file:要加载(执行)的程序名字

    • arg以及后面:调用该程序的命令

  • 返回值:成功无返回(执行指定程序去了),失败返回-1

该函数常用来调用系统程序。如ls、date、cp、cat等命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//execlp():是让子线程不执行父线程的代码,而是执行/execlp所指定的代码或函数
int main(int argc,char* argv[]){
pid_t pid = fork(); //生成一个子线程
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){ //子进程
//ls:要执行的系统函数名;ls -l -d -h:是执行的代码(命令);NULL:是哨兵,代表着-l、-d等参数的结束
execlp("ls","ls","-l","-h",NULL); //当子线程调用execlp失败时,执行下面代码
//execlp("date","date",NULL);
perror("exec error");
exit(1);
}else if(pid > 0){ //父进程
sleep(1);
printf("我是父进程: %d\n",getpid());
}
return 0;
}

3.3 execl函数

功能:既可以执行自己写的程序,也可用执行系统程序

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

  • 参数:

    • path:程序的路径

    • arg以及后面:执行该程序的命令

  • 返回值:成功无返回(执行指定程序去了),失败返回-1

该函数是通过 路径+程序名 来加载进程

案例:将ps aux打印的内容写到一个文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//execl():是让子线程不执行父线程的代码,而是执行execl所指定的文件
//注:execl()也能像execlp()函数那些去执行系统的一些程序,如date、ls等
int main(int argc,char* argv[]){
pid_t pid = fork(); //生成一个子线程
if(pid == -1){
perror("fork error");
exit(1);
}else if(pid == 0){ //子进程
// ./execlp:是路径;./execlp是命令
//execlp("./execlp","./execlp",NULL); //子线程去执行execlp程序里面的代码
execl("/bin/ls","ls","-l",NULL);
perror("exec error");
exit(1);
}else if(pid > 0){ //父进程
sleep(1);
printf("我是父进程: %d\n",getpid());
}
return 0;
}

3.4wait()函数

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

该函数的三个功能:

  • 阻塞等待子进程退出;

  • 回收子进程残留资源

  • 获取子进程结束状态(退出原因)

pid_t wait(int *status);

  • 参数:status是传出参数,结合系统提供的宏函数,可以得到子进程的一些信息

  • 返回值:成功:回收子进程的id号;失败:-1(没有子进程)

可使用wait函数传出的参数status来保存进程的退出状态:

  • WIFEXITED(status)为非0,表示进程正常结束

    • 如上正常退出,使用WEXITSTATUS(status)来获取进程的退出状态(正常的参数)
  • WIFSIGNALED(status)为非0,表示进程异常终止

    • 如上异常退出,使用WTERMSIG(status)来获取使进程终止的那个信号的编号
  • WIFSTOPPED(status)为非0,表示进程处于暂停状态

    • 如上为暂停状态,使用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
//测试:wait函数只有在子线程执行完后才会回收,然后继续向下执行;否则就阻塞在该处,等子线程执行完
int main(void){
pid_t pid,wpid;
int status;
pid = fork(); //创建一个子线程
if(pid == 0){
printf("---child,my id = %d,going to sleep 3s\n",getpid());
sleep(3);
printf("--------------child die------------------\n");
}else if(pid > 0){
wpid = wait(&status); //参数是一个传出参数;返回值是回收的子线程id
if(wpid == -1){
perror("wait error");
exit(1);
}
printf("---parent wait finish:%d\n",wpid); //等子线程执行完后,才会执行该函数
}else{
perror("fork");
return 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
//测试:通过系统提供的宏函数来查看子线程的退出状态信息等
int main(void){
pid_t pid,wpid;
int status;
pid = fork(); //创建一个子线程
if(pid == 0){
printf("我是子线程%d\n",getpid());
printf("--------------child die------------------\n");
return 93;
}else if(pid > 0){
//wpid = wait(NULL); //不关心子进程结束的原因
wpid = wait(&status); //参数是一个传出参数;返回值是回收的子线程id
if(wpid == -1){
perror("wait error");
exit(1);
}
if(WIFEXITED(status)){ //该子线程是否正常终止
printf("我是正常终止,返回%d\n",WEXITSTATUS(status));//打印的值是93,即子线程执行完的返回值
}
if(WIFSIGNALED(status)){ //该子线程是否被信号终止(一切异常终止都是因为信号)
printf("child kill with signal %d\n",WTERMSIG(status)); //打印导致异常退出的信号编号
}
printf("父线程等待完毕,回收的子线程是%d\n",wpid);
}else{
perror("fork");
return 1;
}
return 0;
}

3.5waitpid()函数

pid_t waitpid(pid_t pid , int *status , int optains)

  • 参数:

    • pid:指定某个子进程进行回收(大于0:回收指定ID的子进程;-1:回收任意子进程,相当于wait;0:回收和当前调用waitpid一个组的所有子进程;小于-1:回收指定进程组内的任意子进程)

    • status:是传出参数,结合系统提供的宏函数,可以得到子进程的一些信息

    • options:通过特定参数,可以完成特定功能,如WNOHANG指以非阻塞方式回收

  • 返回值:

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

    • 等于0:参3指定了WNOHANG,并且没有子进程结束会返回0(非阻塞方式)

    • -1:失败,设置errno

waitpid()与wait()的区别是:waitpid能指定某个进程进行回收

注意:一次wait/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
//测试:指定回收某个子线程,且子线程有序打印
int main(int argc,char *argv[]){
int i;
pid_t pid,wpid,tmpid;
for(i=0;i<5;i++){
pid = fork();
if(pid==0){
break; //子线程不用循环,所以直接跳出循环
}
if(i == 2){
tmpid = pid; //此时pid是第三个子线程的pid
printf("指定回收的子线程 = %d\n",tmpid);
}
}
if(i==5){
sleep(5); //如果是不阻塞状态,给主线程添加睡眠,会成功回收指定子线程
//wpid = waitpid(tmpid,NULL,WNOHANG); //指定回收子进程tmpid,并且不阻塞
wpid = waitpid(tmpid,NULL,0); //指定回收子进程tmpid,并且是阻塞状态
if(wpid == -1){
perror("waitpid error");
exit(1);
}
printf("成功回收子线程:%d\n",wpid); //如果不阻塞状态,指定回收的子线程没有结束,则回收没有成功,waitpid会返回0
}else{
sleep(i); //有序打印
printf("我是第%d个子线程%d\n",i+1,getpid());
}
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
//测试:回收多个子线程
int main(int argc,char *argv[]){
int i;
pid_t pid,wpid;
for(i=0;i<5;i++){
pid = fork();
if(pid==0){
break; //子线程不用循环,所以直接跳出循环
}
}
if(i==5){
/*while(wpid = waitpid(-1,NULL,0)){ //循环回收每个子线程,并且是阻塞状态 后面回收完,会返回-1,一直循环
printf("成功回收子线程:%d\n",wpid);
} */
while((wpid = waitpid(-1,NULL,WNOHANG)) != -1){ //以非阻塞状态进行回收
if(wpid>0){ //当成功回收时
printf("成功回收:%d\n",wpid);
}else if(wpid == 0){ //当没有成功回收时,先睡眠1秒
sleep(1);
}
}

}else{
sleep(i);
printf("我是第%d个子线程%d\n",i+1,getpid());
}
return 0;
}

4.IPC方法

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC)。

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。现在常用的进程间通信方式有:

  • 管道(使用最简单)

  • 信号(开销最小)

  • 共享映射区(无血缘关系)

  • 本地套接字(最稳定)

4.1管道

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

  • 其本质是一个伪文件(实为内核缓冲区)

  • 由两个文件描述符引用,一个表示读端(可以有多个),一个表示写端

  • 规定数据从管道的写端流入管道,借助内核缓冲区(4k)实现

1.管道的局限性:

  • 数据不能进程自己写,自己读

  • 管道中数据不可以反复读取,一旦读走,管道中不再存在

  • 采用半双工通信方式,数据只能在单方向上流动

  • 只能在有公共祖先的进程间使用管道。

2.常见的通信方式有:单工通信、半双工通信、全双工通信

3.pipe函数

int pipe(int fd[2]); —> 创建,并打开管道

  • 参数:

    • fd[0]:读端

    • fd[1]:写端

  • 返回值:成功:0;失败:-1,设置errno

4.管道的读写行为:

  • 读管道:

    • 管道中有数据,read返回实际读到的字节数

    • 管道中无数据:(A)管道写端被全部关闭,read返回0(表示读到文件结尾);(B)写端没有全部被关闭,read阻塞等待(不久的将来可能有数据写入),此时会让出CPU

  • 写管道:

    • 管道读端全部被关闭:进程异常终止(也可以使用SIGPIPE信号,使进程不终止)

    • 管道读端没有全部关闭:若管道已满,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
//测试pipe管道
void sys_err(const char*str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024];
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error");
}
pid = fork();
if(pid>0){ //主线程负责写
close(fd[0]); //关闭读端
write(fd[1],str,strlen(str));
sleep(1); //主线程睡眠1秒,防止主线程先结束
close(fd[1]); //写完后,关闭写端
}else if(pid == 0){ //子线程负责读
close(fd[1]); //子线程关闭写端
ret = read(fd[0],buf,sizeof(buf)); //将主线程写在缓冲区的内容读到buf里面
write(STDOUT_FILENO,buf,ret); //将buf的内容打印到屏幕,其中ret为buf读到的字节数
close(fd[0]); //结束后,关闭读端
}
return 0;
}

案例:使用管道实现父子进程间通信,完成ls | wc -l,假定父进程实现ls,子进程实现wc

ls | wc -l 的含义是将ls命令的输出通过管道传递给wc -l命令,然后统计输出的行数。

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
void sys_err(const char *str){    //负责出错时,打印错误
perror(str);
exit(1);
}
int main(){
int fd[2];
int ret;
pid_t pid;
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error");
}
pid = fork();
if(pid == -1){
sys_err("fork error");
//当父进程执行ls时,子进程会永远执行在后面,所以父进程会先结束
}else if(pid == 0){ //子线程
close(fd[0]); //关闭读端
//重定向,相当于屏幕上的内容输入到管道的写端
dup2(fd[1],STDOUT_FILENO); //重定向,让标准输出指向管道的读端,这样输出在屏幕上的就可以写到管道中
execlp("ls","ls",NULL); //父进程执行ls命令
sys_err("exclp ls error");
}else if(pid > 0){ //父线程
close(fd[1]); //关闭写端
//重定向,相当于从管道读端读到的内容写到了标准输入(相当于键盘输入)
dup2(fd[0],STDIN_FILENO); //重定向,让标准输入指向管道的读端,从管道读取数据
execlp("wc","wc","-l",NULL);
sys_err("exclp wc error");
}
return 0;
}

分析:ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc -l正常应该会从stdin读取数据,但此时会从管道的读端读。

案例:使用管道实现兄弟进程间通信,兄:ls,弟:wc -l,父:等待回收子进程。

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
//用兄弟进程实现
//ls | wc -l 的含义是将ls命令的输出通过管道传递给wc -l命令,然后统计输出的行数
int main(){
int fd[2];
int ret;
pid_t pid;
ret = pipe(fd);
if(ret == -1){
sys_err("pipe error"); //一个出错处理函数
}
int i;
for(i=0;i<2;i++){
pid = fork(); //创建子线程
if(pid == -1){
sys_err("fork error");
}
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);
sys_err("exclp ls error");
}else if(i == 1){ //弟进程执行
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
sys_err("exclp wc error");
}
return 0;
}

4.2 FIFO

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程;但通过FIFO,不相关的进程也能交换数据。FIFO是Linux基础文件类型中的一种。但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信,并且可以有多个写端和多个读端,但读完就没有了。

创建管道方式:

  • 终端:mkfifo 命名管道文件名

  • 代码:int ret = mkfifo(“my_mkfifo”,0664);返回-1未成功创建

注意:可以通过终端mkfifo 命名管道文件名 来创建,也可以通过c程序

写端的代码如下:

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
// 通过命名管道对两个无血缘关系的进程进行通信
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
int fd,i;
char buf[4096];
if(argc<2){ //因为这里面没有定义管道,所以传参时必须要传命名管道
printf("ENter like this:./a.out fifoname\n");
return -1;
}
fd = open(argv[1],O_WRONLY); // 以只写的方式打开命名管道的写端
if(fd < 0){
sys_err("open error");
}
i = 0;
while(1){
sprintf(buf,"hello lxx %d\n",i++); //将要写的内容存入buf
write(fd,buf,strlen(buf)); //将buf中的内容写到命名管道(文件)中
sleep(1);
}
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
// 通过命名管道对两个无血缘关系的进程进行通信
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
int fd,len;
char buf[4096];
if(argc<2){ //因为这里面没有定义管道,所以传参时必须要传命名管道
printf("ENter like this:./a.out fifoname\n");
return -1;
}
fd = open(argv[1],O_RDONLY); // 以只读的方式打开命名管道的写端
if(fd < 0){
sys_err("open error");
}
while(1){
len = read(fd,buf,sizeof(buf)); //将从命名管道中读到的数据放到buf中
write(STDOUT_FILENO,buf,len); //将buf中的内容读到标准输出(屏幕)
sleep(3);
}
close(fd);
return 0;
}

4.3存储映射I/O

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

使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现

1.创建映射区

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

  • 参数:

    • addr:指定映射区的首地址。通常传NULL,表示让系统自动分配

    • length:共享内存映射区的大小。(<=文件的实际大小)

    • prot:共享内存映射区的读写属性。PROT_READ、PROT_WRITE

    • flags:标注共享内存的共享属性。MAP_SHARED、MAP_PRIVATE(私有:对内存的操作不会反映到物理磁盘上)

    • fd:用于创建共享内存映射区的那个文件的文件描述符。

    • offset:默认0,表示映射文件全部。偏移位置需是4K的整数倍。

  • 返回值:成功:映射区的首地址;失败:MAP_FAILED,设置errno

2.释放映射区

int munmap(void *addr , size_t length);

参数:

  • addr:mmap的返回值

  • length:大小

3.使用注意事项:

  • 用于创建映射区的文件大小为0,实际指定非0大小创建映射区,出“总线错误”;

  • 用于创建映射区的文件大小为0,实际指定0大小创建映射区,出“无效参数”;

  • 用于创建映射区的文件读写属性为只读,映射区属性为读、写,出“无效参数”;

  • 创建映射区,需要read权限(因为创建时,需要查看映射指定的文件);当访问权限指定为MAP_SHARED(共享)时,mmap的读写属性应该<=文件的open权限(只给mmap写属性不可以)–>因为共享时,对内存操作都会同样的对磁盘(文件)操作;

  • 文件描述符fd,在mmap创建映射区完成即可关闭,后续访问文件用地址访问;

  • 映射区访问权限为MAP_PRIVATE(私有)时,对内存所做的所有修改,只在内存有效,不会反应到物理磁盘上

  • 映射区访问权限为MAP_PRIVATE(私有)时,在需要open文件时,有读权限来创建映射区即可;

4.mmap函数的保险调用:

  • fd = open(“文件名”,O_RDWR);

  • mmap(NULL,有效文件大小,PROT_READ|PROT_WRITE,MAP_SHARED,fd,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
//测试创建一个映射区
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
char *p = NULL;
int fd;
fd = open("testmap",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd == -1){
sys_err("open error");
}
/*lseek(fd,19,SEEK_END);
write(fd,"\0",1); */ //这两行函数与下面的ftruncate()一样
ftruncate(fd,20); //对文件扩容 注意:需要有写权限才能扩容
int len = lseek(fd,0,SEEK_END); //得出文件的大小
p = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); //系统自动分配映射区首地址;映射区的大小;创建的映射区可读可写;映射区是共享的(内存内改,磁盘也改);文件描述符;默认0
if(p == MAP_FAILED){ //系统提供的默认宏
sys_err("mmap error");
}
//使用p对文件进行读写操作
strcpy(p,"hello mmap"); //写操作,将hello maap写到映射区(磁盘)
printf("-----------%s\n",p); //将映射区内的东西写到屏幕
int ret = munmap(p,len); //对映射区进行释放
if(ret == -1){
sys_err("imunmap error");
}
return 0;
}

5.mmap进程通信

  • 父子进程通信

    • 父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
      • MAP_PRIVATE(私有映射):父子进程各自独占映射区(修改,互相都看不到);
      • MAP_SHARED(共享映射):父子进程共享映射区;
  • 无血缘关系间的进程通信

    • 两个进程打开同一个文件(创建的映射区);一个进程写入,另外一个进程读出。

mmap:数据可以重复读取(当创建的映射区为4字节时,只要读的速度快于写的速度,那么可以重复读同一个正整数多次,直到新数据写入);而fifo只能读取数据一次。

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
//父子进程实现mmap映射区之间的通信
int var = 100;
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(void){
int *p;
pid_t pid;
int fd = open("temp",O_RDWR|O_CREAT|O_TRUNC,0644);
if(fd == -1){
sys_err("open error");
}
ftruncate(fd,4); //对文件扩容 注意:需要有写权限才能扩容
int len = lseek(fd,0,SEEK_END); //得出文件的大小
p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); //MAP_SHARED如果是私有的话,表示映射区归父进程私有,也归子进程私有,对其修改,互相都收不到
if(p == MAP_FAILED){ //系统提供的默认宏
sys_err("mmap error");
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork(); //创建子线程
if(pid == 0){
*p = 2000; //子进程写共享内存
var = 1000; //对全部变量,读时共享,写时复制
printf("child,*p = %d,var = %d\n",*p,var);
}else if(pid > 0){
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;
}
//输出的结果是:
child,*p = 2000,var = 1000
parent,*p = 2000,var = 100

5.信号

信号是一种软件中断,通知程序某种事件的发生。常见的信号有SIGABRT(当进程调用abort函数的时候自动发送), SIGALRM(当timer被触发的时候自动发送),等等。

5.1常识

1.信号的共性:简单、不能携带大量信息、满足条件才发送

2。信号的特质:信号是软件层面上的“中断”。一旦信号产生,无论程序执行到什么位置,必须立即停止运行,处理信号,处理结束,再继续执行后续指令。所有信号的产生及处理全部都是由内核完成的

3.产生信号

  • 按键产生,如Ctrl+c、Ctrl+z、Ctrl+\

  • 系统调用产生,如kill、raise、abort

  • 软件条件产生,如定时器alarm

  • 硬件异常产生,如非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)

  • 命令产生:如kill命令

4.信号的状态

  • 递达:产生并且到达进程,可以直接被内核处理掉

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

5.信号处理的方式:执行默认动作、忽略(丢弃)、捕抓(自定义)

6.阻塞信号集(信号屏蔽字):本质就是位图。用来记录信号的屏蔽状态。一旦被屏蔽的信号,再解除屏蔽前,就一直处于未决态

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

5.2信号四要素及常规信号

信号使用之前,应先确定其四要素,而后再用。即信号编号、信号名称、信号对应事件、信号默认处理动作。

1.常规信号:

  • 1)SIGHUP:当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程。

  • 2)SIGINT:当用户按下了<Ctrl+c>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。

  • 3)SIGQUIT:当用户按下<Ctrl+>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号,默认动作为终止进程。

  • 5)SIGTRAP:该信号由断点指令或其他trap指令产生,默认动作为终止进程,并产生core文件。

  • 6)SIGABRT:调用abort函数时产生该信号,默认动作为终止进程并产生core文件。

  • 7)SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。

  • 8)SIGFPE:在发生致命的运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误,默认动作为终止进程并产生core文件。

  • 9)SIGKILL:无条件终止进程,本信号不能被忽略处理和阻塞,默认动作为终止进程,它向系统管理员提供了可以杀死任何进程的方法。

  • 10)SIGUSR1:用户定义的信号,即程序员可以在程序中定义并使用该信号,默认动作为终止进程。

  • 11)SIGSEGV:指示进程进行了无效内存访问,默认动作为终止进程并产生core文件。

  • 12)SIGUSR2:用户自定义信号,程序员可以在程序中定义并使用该信号,默认动作为终止进程。

  • 13)SIGPIPE:Broken pipe向一个没有读端的管道写数据,默认动作为终止进程。

  • 14)SIGALRM:定时器超时,超时的时间由系统调用alarm设置,默认动作为终止进程。

  • 15)SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止,通常用来要示程序正常退出,执行shell命令kill时,缺省产生这个信号,默认动作为终止进程。

  • 17)SIGCHLD:子进程状态发生变化时,父进程会收到这个信号,默认动作为忽略这个信号。

  • 18)SIGCONT:如果进程已停止,则使其继续运行,默认动作为继续/忽略。

  • 19)SIGSTOP:停止进程的执行,信号不能被忽略处理和阻塞,默认动作为暂停进程。

注意:只有每个信号所对应的事情发生了,该信号才会被递送(但不一定递达),不应该乱发信号。

2.kill函数与kill命令

  • kill函数:给指定进程发送指定信号(不一定是杀死)

int kill(pid_t pid , int signum);

  • 参数pid: >0:发送信号给指定进程。=0:发送信号给跟调用kill函数的那个进程处于同一进程组的进程。<-1:取绝对值,发送信号给该绝对值所对应的进程组的所有组员。=-1:发送信号给,有权限发送的所有进程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//测试用kill杀死子进程
void sys_err(const char *str){
perror(str);
exit(1);
}
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); //发送 SIGKILL 信号给其父进程
}
}

kill命令:如杀死一个进程(kill -9 进程号)

5.3 alarm函数

设置定时器(闹钟),在指定seconds后,内核会给当前进程发送SIGALRM(14)信号,进程收到该信号,默认终止动作。

每个进程都有且只有唯一个定时器。

unsigned int alarm(unsigned int seconds);

  • 参数:定时秒数

  • 返回值:上次定时剩余秒数,无错误现象。

常用:取消定时器alarm(0),返回旧闹钟剩余的秒数

注意:定时与进程状态无关,无论进程处于何种状态,alarm都计时。

1
2
3
4
5
6
7
8
9
//用alarm测试1秒可以数多少次
int main(void){
int i;
alarm(1);
for(i = 0;;i++){
printf("%d\n",i);
}
return 0;
}

[补]使用time命令查看程序执行的时间(如time ./alarm_count),alarm_count是上面代码的函数

得出:实际执行时间 = 系统时间+用户时间+等待时间(最多)—->程序运行的瓶颈在于IO,优化程序,首选优化IO。

5.4setitimer函数

setitimer函数可以替代alarm函数,精度到微妙,还可以实现周期定时

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

  • 参数:

    • new_value:定时秒数(结构体类型见代码)

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

  • 返回值:

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

提示:

  • it_interval:用来设定两次定时任务之间间隔时间

  • it_value:定时的时长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void myfunc(int signo){
printf("hello world\n");
}
int main(void){
struct itimerval it,oldit;
signal(SIGALRM,myfunc); //注册SIGALRM 信号的捕抓处理函数

it.it_value.tv_sec = 2; //设置定时器闹钟为2秒
it.it_value.tv_usec = 0; //这是设置微妙的

it.it_interval.tv_sec = 5; //周期为5秒,每5秒提醒一次
it.it_interval.tv_usec = 0;

if(setitimer(ITIMER_REAL,&it,&oldit) == -1){
perror("setitimer error");
return -1;
}
while(1); //循环一直停留在终端,方便观看
return 0;
}

5.5信号集和未决信号集

1.信号集操作函数

  • 自定义信号集(用于和mask发生或与关系):sigset_t set;

  • 清空信号集(全置为0):sigemptyset(sigset_t *set);

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

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

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

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

2.设置信号屏蔽字和解除屏蔽

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

  • how:SIG_BLOCK是设置阻塞;SIG_UNBLOCK是取消阻塞;SIG_SETMASK是用自定义set替换mask

  • set:自定义set

  • oldset: 旧有的mask

用来屏蔽信号、解除屏蔽也使用该函数。其本质是读取或修改进程的信号屏蔽字(PCB中)

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

3.查看未决信号集

int sigpending(sigset_t *set);

  • 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//对信号2设置了阻塞,并查看了未决信号集,Ctrl+c可以进行查看
void sys_err(const char *str){
perror(str);
exit(1);
}
void print_set(sigset_t *set){ //自定义函数打印位图
int i;
for(i=1;i<32;i++){
if(sigismember(set,i)){ //信号i是否在信号集set上
putchar('1');
}else{
putchar('0');
}
}
printf("\n");
}
int main(int argc,char *argv[]){
sigset_t set,oldset,pedset;
int ret = 0;
sigemptyset(&set); //清空信号集(置为0)
//将信号添加到自定义集合中(在自定义集合中,将对应信号置为1)
sigaddset(&set,SIGINT); // Ctrl+c对应信号置为1
sigaddset(&set,SIGQUIT); // Ctrl+\对应信号置为1
sigaddset(&set,SIGKILL); // 由于信号9不能被修改,所以不能被处理,可以通过kill -9 该程序对应进程号 来终止该进程
ret = sigprocmask(SIG_BLOCK,&set,&oldset); //设置阻塞;自定义集合的地址,传出参数
//通过上一行代码,对信号2进行了阻塞,只有解除阻塞,否则信号2一直未决,即在未决信号集中,信号2一直为1
if(ret == -1){
sys_err("sigprocmask error");
}
int i;
for(i=0;i<100;i++){
ret = sigpending(&pedset); //查看未决信号集,参数是传出参数,是该进程的一个未决信号集
if(ret == -1){
sys_err("sigpending error");
}
print_set(&pedset);
sleep(1);
if(i==10){ //当i执行到10的时候,解除屏蔽字的阻塞,此时ctrl+c信号递达,退出程序
sigprocmask(SIG_UNBLOCK,&set,&oldset);
}
}
return 0;
}

5.6sigaction函数

signal函数和sigaction函数都是只注册一个信号的捕抓函数,捕抓是有内核来完成的

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

  • 参数:

    • singum:捕抓的信号

    • act:传入参数,新的处理方式

    • oldact:传出参数,旧的处理方式

信号捕抓的特性:

  • 进程正常运行时,默认PCB中有一个信号屏蔽字,假定为#,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉,捕捉到该信号之后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由#来指定,而是用sa_mask来指定,调用完信号处理函数,再恢复为#。

  • 捕捉函数执行期间,本信号自动被屏蔽(sa_flgs=0)。

  • 捕捉函数执行期间,被屏蔽信号多次发生,解除屏蔽后只处理一次

案例:用signal()函数对信号2进行捕抓

1
2
3
4
5
6
7
8
9
10
11
12
13
void sys_err(const char *str){
perror(str);
exit(1);
}
void sig_cath(int signo){ //自定义捕抓函数,当指定信号产生,就会执行
printf("catch you!%d\n",signo);
return;
}
int main(int argc,char *argv[]){
signal(SIGINT,sig_cath); //当信号SIGINT产生时,对其捕抓,去执行对应捕抓函数
while(1);
return 0;
}

案例:用sigaction()函数对信号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
void sys_err(const char *str){
perror(str);
exit(1);
}
void sig_cath(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_cath; //设置回调函数(捕抓函数)
sigemptyset(&(act.sa_mask)); //将sa_mask屏蔽字置为0,这个只在sig_catch函数工作时有效
act.sa_flags = 0; //默认值(在一个信号的捕抓函数执行时,默认蒙蔽该信号,防止再来)
int ret = sigaction(SIGINT,&act,&oldact); //注册信号捕抓函数
if(ret == -1){
sys_err("sigaction error");
}
ret = sigaction(SIGQUIT,&act,&oldact); //注册第二个信号捕抓函数
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
void sys_err(const char *str){
perror(str);
exit(1);
}
void catch_child(int signo){ //有子进程终止,发送SIGCHLD信号时,该函数会被内核回调
pid_t wpid;
int status;
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 set; //自定义阻塞信号集
sigemptyset(&set);
sigaddset(&set,SIGCHLD); //将自定义阻塞信号集的SIGCHLD信号置1,使其与pcd里面的阻塞信号集作用,让该信号阻塞
sigprocmask(SIG_BLOCK,&set,NULL); //使其与pcd的阻塞信号集作用
int i;
for(i=0;i<5;i++){
if((pid=fork())==0){
break;
}
}
if(i==5){
struct sigaction act;
//初始化act结构体
act.sa_handler = catch_child; //捕捉函数
sigemptyset(&act.sa_mask); //设置捕捉函数执行期间的屏蔽信号集,全置为0
act.sa_flags = 0; //设置默认属性,本信号自动屏蔽
sigaction(SIGCHLD,&act,NULL); //子进程状态发生改变就触发捕捉函数
//解除阻塞(父进程注册完毕,可以开始接收子线程结束后,内核传给父进程的信号了)
sigprocmask(SIG_UNBLOCK,&set,NULL); //如果没有解除阻塞步骤,回调函数没有执行的机会(因为pcd的mask对该信号是屏蔽状态)
printf("我是父进程%d\n",getpid());
while(1);
}else{
printf("我是子进程%d\n",getpid());
return i;
//sleep(i); //可以用sleep进行一个一个回收
}
}

6.会话与守护进程

6.1进程组和会话

进程组,也称之为作业。BSD于1980年前后向Unix中增加的一个新特性。代表一个或多个进程的集合。每个进程都属于一个进程组。在waitpid和kill函数的参数中都曾使用过。操作系统设计的进程组的概念,是为了简化对多个进程的管理。当父进程,创建子进程的时候,默认子进程与父进程属于同一个进程组。进程组ID = 第一个进程ID(组长进程)。所以,组长进程标识,其进程组ID = 其进程ID。

而会话就是进程组的集合。

1.创建一个会话需要注意以下6点:

  • 调用进程不能是进程组组长,该进程变成新会话的首进程
  • 该进程成为一个新进程组的组长进程
  • 需要root权限 (Ubuntu不需要)
  • 新会话丢弃原有的控制终端,该会话没有控制终端
  • 如果该进程调用的是组长进程,则出错返回
  • 建立新会话时,先调用fork,父进程终止,子进程调用setsid()

2.getsid()函数:获取进程所属的会话ID

pid_t getsid(pid_t pid);

  • 参数:pid为所需要获取的进程

  • 返回值:成功(返回调用进程的会话ID);失败(返回-1,设置errno)

注意:组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程

3.setsid()函数:创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID

pid_t setsid(void);

返回值:成功(返回调用进程的会话ID);失败(返回-1)

注意:调用setsid函数的进程,既是新的会长,也是新的组长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//子进程成立新会话(父进程不能创建会话)
int main(void){
pid_t pid;
if((pid = fork())<0){
perror("fork");
exit(1);
}else if(pid == 0){ //子进程执行
printf("child process PID is %d\n",getpid());
printf("Group ID of child is %d\n",getpgid(0)); //组id(父进程的)--->0为默认调用该程序的进程组id
printf("Session ID of child is %d\n",getsid(0)); //会话id(有可能是父进程的,也可能是父进程的父进程的)
sleep(10);
setsid(); //子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组id即为会话进程(子进程id=组进程id=会话进程id)
printf("changed:\n");
printf("child process PID is %d\n",getpid());
printf("Group ID of child is %d\n",getpgid(0)); //组id
printf("Session ID of child is %d\n",getsid(0)); //会话id
sleep(20);
exit(0);
}
return 0;
}

6.2守护进程

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

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

2.守护进程的创建步骤:

  • fork子进程,让父进程终止

  • 子进程调用setsid()创建新会话

  • 通常根据需要,改变工作目录位置chdir()

  • 通常根据需要,重设umask文件权限掩码

  • 通常根据需要,关闭/重定向文件描述符012

  • 守护进程 业务逻辑。—->while()

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
//守护进程的创建(运行在后台)
void sys_err(const char *str){
perror("str");
exit(1);
}
int main(int argc,char *argv[]){
pid_t pid;
int ret,fd;
//1、创建子进程,父进程终止
pid = fork();
if(pid>0){
exit(0); //父进程终止
}
//2、子进程创建新会话
//printf("---------------------\n");
pid = setsid();
if(pid == -1){
sys_err("setsid error");
}
//3、改变工作目录位置
ret = chdir("/home/c_c++后端/系统编程/session_dir");
if(ret == -1){
sys_err("chdir error");
}
//4、改变文件访问权限掩码
umask(0022);
//5、关闭文件描述符 (此时就可用的最小文件描述符是3)
close(STDIN_FILENO); //关闭文件描述符0
fd = open("/dev/null",O_RDWR); //读写打开dev/null文件 ----->fd=0
if(fd == -1){
sys_err("open error");
}
dup2(fd,STDOUT_FILENO); //将文件描述符1指向fd
dup2(fd,STDERR_FILENO); //将文件描述符2指向fd
//6 一直运行,等待接收命令
while(1); //模拟守护进程业务
//printf("------------------\n");
return 0;
}

7.线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程是独立调度和分派的基本单位,同一进程中的多条线程将共享该进程中的全部系统资源,但同一进程中的多个线程有各自的调用栈、寄存器环境和线程本地存储。

1.线程与进程区别:

  • 进程:有独立的进程地址空间,有独立的pcb

  • 线程:有独立的pcb,没有独立的进程地址空间—>是轻量级进程LWP

注意:线程是最小的执行单位;进程是最小分配资源单位,可看成是只有一个线程的进程。进程里创建线程后,进程也叫线程了

2.查看某个进程里的线程:

  • ps -Lf 进程pid:线程号是LWP那一栏(相当于进程号,线程号是接在进程号后面的)

3.线程共享资源:文件描述符、每种信号的处理的方式、当前工作目录、用户ID和组ID、内存地址空间(.text/.data/.bss/heap/共享库)、全局变量;

4.线程非共享资源:线程id、处理器现场和栈指针(内核栈)、独立的栈空间(用户空间栈)、errno变量、信号屏蔽字、调度优先级

5.线程优缺点

  • 优点:提高程序并发性、开销小、数据通信、共享数据方便;

  • 缺点:是库函数,不稳定、调试、编写困难、对信号支持不好

7.1线程控制原语

1.线程函数

pthread_t pthread_self(void);

  • 返回值:本线程id号

  • 作用:获取线程id,线程id是在进程地址空间内部用来标识线程身份的

注意:线程ID是在进程中来标识线程身份的,进程通过线程ID来对其加以区分;而LWP是线程号,标识线程身份给CPU用的(CPU用线程号来划分时间片)

2.创建线程

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void*),void *arg);

  • 参1:传出参数,表示新创建的子线程id

  • 参2:线程属性,传NULL表示使用默认属性

  • 参3:子线程回调函数,若创建成功,pthread_create函数返回时,该函数会被自动调用

  • 参4:参数3函数的参数,没有的话就传NULL

  • 返回值:成功(返回0);失败(errno)

注意:主函数利用pthread_create()创建新线程时,主函数会立即返回值并执行下面代码,不会因为新线程没有执行完回调函数而阻塞。

当新线程启动后,它会在后台执行回调函数,同时主线程继续执行自己的任务。如果回调函数执行时间较长,主线程仍然不会被阻塞,它会继续往下执行,直到遇到需要等待新线程完成任务的代码段。

3.退出当前线程

void pthread_exit(void *retval);

  • 参数:退出值,无退出值时,NULL

注意:exit()是退出当前进程;return;是返回到调用者那里去

pthread_exit(NULL)的作用是在线程中显式地退出线程的函数调用。它用于终止当前线程的执行,并将线程的退出状态设置为NULL。当调用pthread_exit(NULL)时,当前线程会立即退出,并将控制返回给创建该线程的线程。这意味着线程的执行会终止,但其他线程仍然可以继续执行。

4.阻塞等待线程退出,获取线程退出状态

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

  • 参1:要回收的线程id

  • 参2:传出参数,存储线程退出的状态,线程正常退出,得到来自该线程在回调函数中返回的信息;线程异常退出,返回-1.

  • 返回值:成功回收(返回0),失败(errno)

注意:在进程中,进程结束是exit(1),退出状态是int型,所以回收进程wait的参数是int*型;在线程中,线程结束是pthread_exit(void*),退出状态是void*型,所以回收线程pthread_join参数是void**型。

5.杀死(取消)线程,类似于进程中的kill()

int pthread_cancel(pthread_t thread);

  • 参数:待杀死的线程id

  • 返回值:成功(0);失败(errno)

注意:线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。

如果子线程没有到达取消点,那么使用pthread_cancel无效。我们可以在程序中。手动添加一个取消点,使用pthread_testcancel()。

取消点:通常是一些系统调用creat,open,pause,close,read,write。(可以理解为是线程有进入内核,如果线程执行的内容是if,while,for这些则不能用pthread_cancel()杀死)

案例:终止线程的三种方法,注意取消点的概念。

6.线程出错要用这个函数打印

char *strerror(int errnum);

  • 使用:fprintf(stderr,”pthread_join error:%s”,strerror(ret));

7.实现线程分离

int pthread_detach(pthread_t thread);

  • 参数:待分离的线程id

  • 返回值:成功(0),失败(errno)

注意:pthread_detach不能与pthread_join一起用,当使用线程分离时,被分离的子线程就不归主线程管了,子线程执行完由系统回收,不需要主线程再调用pthread_join来回收,如果调用,会回收失败。

8.线程属性,用来设置分离属性

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//编译时:后面要加-pthread
void *tfn(void *arg){ //子线程执行部分
printf("我是子线程,先慢慢执行\n");
sleep(3);
return NULL;
}
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
pthread_t tid;
int ret = pthread_create(&tid,NULL,tfn,NULL); //创建线程执行回调函数
if(ret != 0){
sys_err("pthread_create error");
}
printf("我是父进程,先结束了\n");
pthread_detach(tid);
//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
//编译时:最后面要加-pthread
void *tfn(void *arg){ //子线程执行部分
int i = (int)arg; //将传过来的参数进行转换
sleep(i);
printf("我是第%d子线程:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
return NULL;
}
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
pthread_t tid;
int ret;
int i;
for(i=0;i<5;i++){
ret = pthread_create(&tid,NULL,tfn,(void *)i);
if(ret != 0){
sys_err("pthread_create error");
}
}
sleep(i);
printf("主线程里面,pid=%d,tid=%lu\n",getpid(),pthread_self());
return 0;
}

案例:使用pthread_exit()函数来退出子线程

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
//编译时:最后面要加-pthread
void *tfn(void *arg){ //子线程执行部分
int i = (int)arg;
sleep(i);
if(i==2){
//exit(0); //表示退出进程,所以i=2后的线程都执行不了
pthread_exit(NULL); //将当前线程退出
}
printf("我是第%d子线程:pid=%d,tid=%lu\n",i+1,getpid(),pthread_self());
return NULL;
}
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
pthread_t tid;
int ret;
int i;
for(i=0;i<5;i++){
ret = pthread_create(&tid,NULL,tfn,(void *)i);
if(ret != 0){
sys_err("pthread_create error");
}
}
sleep(i);
printf("主线程里面,pid=%d,tid=%lu\n",getpid(),pthread_self());
return 0;
}

案例:通过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
struct thrd{
int var;
char str[256];
};
void sys_err(const char *str){
perror(str);
exit(1);
}
void *tfn(void *arg){ //方法2:通过在主函数中定义一个结构体变量来实现
struct thrd *tval = (struct thrd *)arg;
tval->var = 93;
strcpy(tval->str,"hello lxx");
return (void *)tval;
}
int main(int argc,char *argv[]){
pthread_t tid;
/*struct thrd *retval;
int ret = pthread_create(&tid,NULL,tfn,NULL);
*/
struct thrd arg; //定义一个结构体变量
struct thrd *retval;
int ret = pthread_create(&tid,NULL,tfn,(void *)&arg);
if(ret != 0){
sys_err("pthread_create error");
}
ret = pthread_join(tid,(void **)&retval); //回收子线程(阻塞)
if(ret != 0){
sys_err("pthread_join error");
}
printf("child thread exit with var = %d,str = %s\n",retval->var,retval->str);
pthread_exit(NULL); //线程退出
}

案例:通过pthread_cancel()终止子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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_detach()设置线程分离

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
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;
int ret = pthread_create(&tid,NULL,tfn,NULL);
if(ret != 0){
fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
exit(1);
}
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); //接收失败会返回非0值
printf("join ret = %d\n",ret);
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);
}

案例:通过线程属性设置分离属性

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
//通过线程属性设置来使子线程分离,这样就可以创建一个是一个了,而不需要后面手动一个一个设置
void *tfn(void *arg){
printf("thread:pid = %d,tid = %lu\n",getpid(),pthread_self());
return NULL;
}
int main(){
pthread_t tid;
pthread_attr_t attr; //定义属性结构体

int ret = pthread_attr_init(&attr); //初始化属性结构体(传出参数)
if(ret != 0){
fprintf(stderr,"attr_init error:%s\n",strerror(ret));
exit(1);
}
ret = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //设置线程属性为 分离属性
if(ret != 0){
fprintf(stderr,"attr_setdetachastate error:%s\n",strerror(ret));
exit(1);
}
ret = pthread_create(&tid,&attr,tfn,NULL);
if(ret != 0){
fprintf(stderr,"pthread_create error:%s",strerror(ret));
}
printf("main:pid = %d,tid = %lu\n",getpid(),pthread_self());
ret = pthread_attr_destroy(&attr); //销毁属性结构体
if(ret != 0){
fprintf(stderr,"attr_destroy error:%s\n",strerror(ret));
exit(1);
}
pthread_exit((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
void *tfn1(void *arg){
printf("thread 1 returning\n");
return (void *)111;
}
void *tfn2(void *arg){
printf("thread 2 returning\n");
pthread_exit((void *)222);
}
void *tfn3(void *arg){
while(1){
printf("thread 3 returning\n");
sleep(1);
}
// pthread_testcancel(); //自动添加取消点
return (void *)333;
}
int main(){
pthread_t tid;
void *tret = NULL;

pthread_create(&tid,NULL,tfn1,NULL);
pthread_join(tid,&tret);
printf("thread 1 exit code = %d\n\n",(int)tret);

pthread_create(&tid,NULL,tfn2,NULL);
pthread_join(tid,&tret);
printf("thread 2 exit code = %d\n\n",(int)tret);

pthread_create(&tid,NULL,tfn3,NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid,&tret);
printf("thread 3 exit code = %d\n",(int)tret);

return 0;
}

7.2线程使用注意事项

线程使用注意事项:

  • 主线程退出其他线程不退出,主线程应该调用pthread_exit()

  • 避免僵尸线程,使用pthread_join、pthread_detach、pthread_create(指定分离属性)

  • malloc和mmap申请的内存可以被其他线程释放(线程共享堆)

  • 应该避免在多线程模型中调用fork。除非马上exec。因为如果调用fork,则子进程中只有调用fork的线程存在,其他线程在子进程中均默认被pthread_exit

  • 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制

8.线程同步

协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。

锁的使用:建议锁,对公共数据进行保护。所有线程应该在访问公共数据前先拿锁再访问。但锁本身不具备强制性。

8.1互斥锁mutex

1.使用互斥锁的一般步骤:

  • pthread_mutex_t lock; //创建锁

  • pthread_mutex_init; //初始化

  • pthread_mutex_lock; //加锁

  • 访问共享数据(stdout)

  • pthread_mutex_unlock(); //解锁

  • pthread_mutex_destroy; //销毁锁

补:restrict关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。

2.初始化互斥锁的两种方法:

  • pthread_mutex_t mutex; //定义一把锁

  • pthread_mutex_t_init(&mutex,NULL); //动态初始化

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

注意:尽量保证锁的粒度(范围),越小越好。访问共享数据前加锁,访问结束立即解锁。

方便记忆:互斥锁,本质是结构体。我们可以看成整数,初值为1,即pthread_mutex_init()调用成功;加锁理解为–操作,阻塞线程;解锁理解为++操作,唤醒阻塞在锁上的线程。

try锁:尝试加锁,成功–操作;失败就返回,同时设置错误号EBUSY。

案例:通过互斥锁进行对公共区域的访问

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
pthread_mutex_t mutex;          //定义一把互斥锁
void *tfn(void *arg){
srand(time(NULL));
while(1){
pthread_mutex_lock(&mutex); //加锁
printf("hello ");
sleep(rand()%3); //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
printf("lxx\n");
pthread_mutex_unlock(&mutex); //解锁
sleep(rand()%3);
}
return NULL;
}
int main(void){
pthread_t tid;
srand(time(NULL));
int ret = pthread_mutex_init(&mutex,NULL); //对锁进行初始化
if(ret != 0){
fprintf(stderr,"mutex init error:%s\n",strerror(ret)); //打印错误信息
exit(1);
}
pthread_create(&tid,NULL,tfn,NULL); //创建线程执行回调函数
while(1){
pthread_mutex_lock(&mutex); //加锁
printf("HELLO ");
sleep(rand()%3); //这里睡眠了1到3秒,但因为有加锁,输出依然是完整的HELLO WORLD
printf("WORLD\n");
pthread_mutex_unlock(&mutex); //解锁
sleep(rand()%3);
}
pthread_join(tid,NULL);
pthread_mutex_destroy(&mutex); //销毁锁
return 0;
}

8.2读写锁

锁只有一把,以读方式给数据加锁为读锁;以写方式给数据加锁为写锁。相较于互斥量(互斥锁)而言,当读线程多的时候,读写锁可以提高访问效率。

1.读写锁的特性

  • 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞

  • 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞

  • 读写锁是“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求,优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。

读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。即写独占,读共享

2.主要应用函数

  • pthread_rwlock_init //自定义读写锁

  • pthread_rwlock_destroy //销毁读写锁

  • pthread_rwlock_rdlock //读模式加锁

  • pthread_rwlock_wrlock //写模式加锁

  • pthread_rwlock_tryrdlock //尝试读模式加锁

  • pthread_rwlock_trywrlock //尝试写模式加锁

  • pthread_rwlock_unlock //读写锁解锁(通用)

以上函数的返回值都是:成功(返回0);失败(返回错误号)

  • pthread_rwlock_t rwlock //定义一个读写锁变量rwlock

3.死锁

是使用锁不恰当导致的现象,如:

  • 对一个锁反复lock

  • 两个线程,各自持有一把锁,请求另一把

案例:模拟3个线程不定时写同一个全局资源,5个线程不定时读同一个全局资源

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
int counter;                  //定义一个全局变量
pthread_rwlock_t rwlock; //全局的读写锁
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 i;
pthread_t tid[8]; //定义一个线程组
pthread_rwlock_init(&rwlock,NULL); //初始化读写锁
for(i = 0; i < 3; i++){
pthread_create(&tid[i],NULL,th_write,(void *)i); //前面三个线程负责写
}
for(i = 0; i < 5; i++){
pthread_create(&tid[i+3],NULL,th_read,(void *)(i+3)); //后面五个线程负责读
}
for(i = 0; i < 8; i++){
pthread_join(tid[i],NULL); //主线程回收子线程
}
pthread_rwlock_destroy(&rwlock); //销毁读写锁
return 0;
}

8.3条件变量

条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用。

1.主要应用函数:

  • pthread_cond_init函数 //初始化条件变量

  • pthread_cond_destroy函数 //销毁条件变量

  • pthread_cond_wait函数 //等待条件满足

  • pthread_cond_timewait函数 //等待条件满足(超时不等)

  • pthread_cond_signal函数 //唤醒阻塞在条件变量的线程(一个)

  • pthread_cond_broadcast函数 //唤醒阻塞在条件变量的线程(多个)

以上函数返回值:成功(返回0);失败(返回错误号)

  • pthread_cond_t cond; //定义一个条件变量cond

2.初始化条件变量的两种方法:

pthread_cond_t cond; //定义一个条件变量

  • pthread_cond_t_init(&cond,NULL); //动态初始化

  • pthread_cond_t cond=PTHREAD_MUTEX_INITIALIZER; //静态初始化

3.pthread_cond_wait函数作用:

  • 阻塞等待条件变量cond(参1)满足

  • 释放已掌握的互斥锁,相当于pthread_mutex_unlock(&mutex)。

注意:上面两步为一个原子操作(中间不会分开执行)

  • 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)

案例:模拟一个消费者-生产者模式

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
void err_thread(int ret,char*str){        //线程创建失败调用的打印错误信息的函数
if(ret != 0){
fprintf(stderr,"%s:%s\n",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 = malloc(sizeof(struct msg)); //创建一个节点
mp->num = rand()%1000+1; //模拟随机产生一个数值
printf("--------produce %d\n",mp->num); //打印生产者产生的数据
//后插法生产数据(将数据结点连起来)
pthread_mutex_lock(&mutex); //加锁,互斥锁
mp->next = head;
head = mp;
pthread_mutex_unlock(&mutex); //解锁互斥锁

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

}
void *consumer(void *arg){ //消费者的回调函数:消费数据
while(1){
struct msg *mp;
pthread_mutex_lock(&mutex); //加锁,互斥量
while(head == NULL){ //当是空的时候,才对消费者进行阻塞等待,阻塞期间,会解锁,让生产者拿锁生产数据(如果是只有一个消费者可以是if,多个消费者则需要while)
pthread_cond_wait(&has_data,&mutex); //阻塞等待条件变量,当有数据时,消费者就会被激活,且自动加锁
} //如果有多个消费者,而用的是if,则会出现没有数据时,消费者还在消费,即吐核问题
mp = head;
head = mp->next;
pthread_mutex_unlock(&mutex);
printf("-------------cconsumer id:%lu : %d\n",pthread_self(),mp->num);
free(mp);
sleep(rand()%3);
}
return NULL;
}
int main(int argc,char *argv[]){
int ret;
pthread_t pid,cid1,cid2;
srand(time(NULL));

ret = pthread_create(&pid,NULL,produser,NULL); //创建一个生产者线程
err_thread(ret,"pthread_create produser error");

ret = pthread_create(&cid1,NULL,consumer,NULL); //创建第一个消费者线程
err_thread(ret,"pthread_create consumer1 error");

ret = pthread_create(&cid2,NULL,consumer,NULL); //创建第二个消费者线程
err_thread(ret,"pthread_create consumer2 error");
pthread_join(pid,NULL); //回收线程
pthread_join(cid1,NULL); //回收线程
pthread_join(cid2,NULL); //回收线程
return 0;
}

9.信号量

应用于线程、进程间同步,因为互斥锁对于多个线程访问同一个公共内存空间时,只能一个一个访问,虽然保证了数据正确性的目的,但导致了线程的并发性下降。

信号量是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。

1.主要应用函数

  • sem_init //信号量初始化

  • sem_destroy //信号量销毁

  • sem_wait //信号量加锁

  • sem_trywait

  • sem_timedwait

  • sem_post //信号量解锁

以上函数返回值:成功(返回0);失败(-1)

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

  • 参1:信号量

  • 参2:0—>用于线程间同步;1—>用于进程间同步

  • 参3:N值(指定同时访问的线程数)

3.sem_wait():一次调用,做一次–操作,当信号量的值为0时,再次–就会阻塞(对比pthread_mutex_lock)

4.sem_post():一次调用,做一次++操作,当信号量的值为N时,再次++就会阻塞(对比pthread_mutex_unlock)

案例:用信号量实现生产者-消费者模式

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
#define NUM 5
int queue[NUM]; //全局数值实现环形队列
sem_t blank_number,product_number; //空格子信号量,产品信号量
//开始时,空格子为N,产品格子为0
void *producer(void *arg){
int i=0;
while(1){
sem_wait(&blank_number); //生产者将格子数--,为0则阻塞等待(说明产品满了)
queue[i]=rand()%100+1; //生产一个产品
printf("------produce------%d\n",queue[i]);
sem_post(&product_number); //将产品数++

i = (i+1)%NUM; //借助下标实现环形
sleep(rand()%1);
}
}
void *consumer(void *arg){
int i=0;
while(1){
sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待(说明还没有产品可以消费)
printf("-----consumer------%d\n",queue[i]);
queue[i]=0; //消费一个产品
sem_post(&blank_number); //消费掉以后,将空格子数++
i = (i+1)%NUM;
sleep(rand()%3);
}
}
int main(){
pthread_t pid,cid;
sem_init(&blank_number,0,NUM); //初始化空格子信号量为5,0表示线程间同步
sem_init(&product_number,0,0); //初始化产品数信号量为0

pthread_create(&pid,NULL,producer,NULL); //创建生产者
pthread_create(&cid,NULL,consumer,NULL); //创建消费者

pthread_join(pid,NULL);
pthread_join(cid,NULL);

sem_destroy(&blank_number);
sem_destroy(&product_number);
return 0;
}