学习地址
Linux高级编程,也称为系统编程,是在应用层编程,更具体的说是内核向应用程序提供的接口层。
【稳定代码】解释:
c语言
属于应用层,它封装的 printf
打印函数内部是怎样做到的?
其实就是
Linux内核
里面的write函数
,甚至可以直接用write函数
打印。
一。文件IO(输入输出)
实现 cp
命令:
原理:
int main(int argc, char *argv[])
{
int rd_fd, wr_fd; //读写输出
char read_buf[128] = {0}; //用户空间缓存
int rd_ret = 0; //找个中间空间
if (argc < 3)
{
printf("Please input src and des file\n");
return -1;
}
rd_fd = open(argv[1], O_RDONLY); //open src 空文件所以不要 O_CREAT
if (rd_fd < 0)
{
printf("open src file %s\n fail", argv[1]);
return -2;
}
printf("open src file %s success, rd_fd=%d\n", argv[1], rd_fd); //3
wr_fd = open(argv[2], O_WRONLY); //open des
if (wr_fd < 0)
{
printf("open des file %s\n fail", argv[2]);
return -3;
}
printf("open des file %s success wr_fd=%d\n", argv[2], wr_fd); //4
while (1) //有弹幕说 do while 更好
{
rd_ret = read(rd_fd, read_buf, 128);
if (rd_ret < 128)
break;
write(wr_fd, read_buf, rd_ret);
memset(read_buf, 0, 128); //清空全部填充为0 有弹幕说可以不用
}
write(wr_fd, read_buf, rd_ret); //再写一次防止最后一部分小于128的部分没写到
close(rd_fd);
close(wr_fd);
return 0;
}
- touch
des.c
src.c
(创建俩空白文件)- ./hello
src.c
des.c
(hello为该函数)- diff
src.c
des.c
(查看区别)- echo blablabla >
src.c
(可以写东西到文件)- cat
des.c
(查看文件内容)
目录流I/O
#include
#include
int main(int argc, char *argv[])
{
int ret;
DIR *dp;
struct dirent *dir; //读取目录后的返回值是一个结构体
long loc; //目录位置
ret = mkdir("./mydir", 0777);
if (ret < 0)
{
printf("mkdir mydir failed\n");
return -1;
}
printf("create mydir success\n");
dp = opendir("./mydir");
if (dp == NULL)
{
printf("open mydir failed\n");
return -2;
}
printf("open mydir success\n");
//因为目录的内容(子目录名+目录下文件的信息)存储是一个像链表一样的 【radix基数树】,
//要一直从首节点打印完,不加循环只能打出第一个结点的data
while (1)
{
dir = readdir(dp);
loc = telldir(dp); //还有调整 seekdir(dp, 0) 以及 rewinddir(dp)
if (dir != NULL)
{
printf("inode=%ld,name=%s\n", dir->d_ino, dir->d_name);
printf("loc=%ld\n", loc);
}
else
{
break;
}
}
closedir(dp);
return 0;
}
综合应用:单机模式下的文件上传功能
int main(int argc, char *argv[])
{
DIR *dp;
int src_fd, des_fd; //上传下载时文件的描述符
int fd, ret; //到底读取到多少的返回值
struct dirent *dir;
char server[128] = {0}; //读取目录缓存
char file[128] = {0}; //读取文件缓存
char buf[128] = {0}; //进行读的缓存
start:
printf("Please input server PATH and Directory name.\n");
scanf("%s", server);
//打开一个目录
dp = opendir(server);
if (dp == NULL)
{
printf("open server:%s\n failure\n", server);
goto start;
}
printf("open server:%s\n success\n", server);
while (1) //展示目录下面的子目录和文件信息
{
dir = readdir(dp);
if (dir == NULL)
break;
else
{
printf("inode=%llu\t file_name=%s\n", dir->d_ino, dir->d_name);
}
}
printf("Please input download file name:\n");
scanf("%s", file);
//文件下载 用的文件I/O 不是fopen标准I/O
//绝对路径(全路径)比如:/Users/me/Desktop /linux_demo/xxxfile
//第一个拼接/linux_demo, 第二个拼接/file_name
src_fd = open((strcat(strcat(server, "/"), file)), O_RDONLY);
if (src_fd < 0)
{
printf("open download file:%s failed\n", file);
return -1;
}
printf("open download file:%s ok\n", file);
//进行目标文件拷贝
des_fd = open(file, O_WRONLY | O_CREAT, 0777);
if (des_fd < 0)
{
printf("create destination file:%s failed\n", file);
return -2;
}
printf("create destination file:%s ok\n", file);
//拷贝写入✏️
while (1)
{
ret = read(src_fd, buf, 128);
if (ret < 128)
break;
write(des_fd, buf, ret);
}
write(des_fd, buf, ret); //写小于128的
close(src_fd);
close(des_fd);
closedir(dp);
return 0;
}
二。进程(单机通信)
管道
就是一块缓存,以队列形式存在于内核。
- 无名管道 2. 有名管道
队列的数据结构
FIFO(first in first out)
int main(int argc, char *argv[])
{
int ret;
int fd[2] = {0};
int buf[128] = {0};
char write_buf[] = "I am Pipe";
char read_buf[128] = {0};
ret = pipe(fd);
if (ret < 0)
{
printf("Create pipe failed\n");
return -1;
}
printf("Create pipe success! fd[0] = %d, fd[1] = %d\n", fd[0], fd[1]);
ret = write(fd[1], write_buf, sizeof(write_buf));
if (ret < 0)
printf("write pipe failed\n");
printf("write pipe success!\n");
read(fd[0], read_buf, 128);
printf("readbuffer is: %s\n", read_buf);
//读完之后管道就释放了,在读就没有了会一直停在那儿(读阻塞)
// memset(read_buf, 0, 128);
// read(fd[0], read_buf, 128);
// printf("这句话就打印不出来,会卡在上面");
return 0;
}
父子进程通过无名管道pipe
通信
int main(int argc, char *argv[])
{
pid_t pid; //子进程
char process_inter = 0; //用一个变量,放到管道里,来实现进程之间的通信
int ret, i; //pipe 的返回值
int fd[2]; //管道的两端
ret = pipe(fd);
if (ret < 0)
{
printf("create pipe failed\n");
return -1;
}
printf("create pipe success\n");
pid = fork();
if (pid == 0)
{
//如果父进程没有写入,会在这儿卡着,打印不错后面 进程状态为sleep
read(fd[0], &process_inter, 1);
while (process_inter == 0)
;
for (i = 0; i < 5; i++)
{
printf("I am Child Process\n");
}
}
if (pid > 0)
{
for (i = 0; i < 5; i++)
{
printf("I am Parent Process\n");
}
sleep(3);
process_inter = 1;
write(fd[1], &process_inter, 1);
}
return 0;
}
非亲属进程之间的有名管道通信
有名管道文件必须 只读只写成对 打开时才会继续执行,否则程序会进入睡眠模式
代码在这儿:https://gist.github.com/zwsnail/6832f9f5071f911e6593933029edce27
信号
内核中已经存在信号,不像管道要创建管道文件。
自身两个进程不发信号,只有【Linux 内核】来发送。
属于不精确通信,信号只能告诉进程大概发生了什么事情,但是不能准确的告诉进程详细的细节信息。(像放狼烟 的信号,只知道有敌人来了,不知道来了多少,谁来了...)
实现 KILL 函数
用mykill.c
来杀死一个死循环程序,就实现了
kill -9 5858
杀死进程
raise函数:只能让自己这个进程 (停止,杀死...自己选)
exit()
函数与_exit()
函数最大的区别
就在于exit()
函数在调用exit
系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是"清理I/O缓冲"
。
return、exit和_exit的区别:**return
和exit
效果一样,都是会执行进程终止处理函数,但是用_exit
终止进程时并不执行atexit
注册的进程终止处理函数。
父子进程间,wait()
和 waitpid()
函数的作用以及 SIGTSTP
信号的例子。
waitpid()
:
如果在调用waitpid()
函数时,当指定等待的子进程已经停止运行或结束了,则waitpid()
会立即返回;但是如果子进程还没有停止运行或结束,则调用waitpid()
函数的父进程则会被阻塞,暂停运行。
函数详情:https://baike.baidu.com/item/waitpid (例子中父进程必须等到了waitpid
才会执行, 而注释掉这行就直接 子sleep
然后 父退出)
int main()
{
pid_t pid;
pid = fork();
if (pid > 0)
{
sleep(8);
if (waitpid(pid, NULL, WNOHANG) == 0)//成功执行 0,不成功 -1
{
kill(pid, SIGKILL); //9
}
while (1)
;
}
if (pid == 0)
{
printf("raise function before\n");
raise(SIGTSTP);
printf("raise function after\n");
exit(0);
}
return 0;
}
更多资料 :孤儿进程与僵尸进程
alarm函数:只发一种闹钟 ⏰ 信号 ---- SIGALRM
定时一段时间,再发终止信号,而且只能发当前自己这个进程。
信号的接收
sleep(4)
while(1)
pause()
pause
和sleep
的区别:
pause
没有参数,是一直持续sleep
,直到被唤醒。而sleep
是有参数,时间设定的。
信号的自定义处理 signal()
PS:sigaction
函数类似于signal
函数,完全可以替代后者,也更加稳定。
有时候我们希望对信号作出及时的反映的,比如当用户按下Ctrl+C
时,我们不想什么事情也不做,我们想告诉用户你的这个操作不好,请不要重试,而不是什么反映也没有的。 这个时候我们要用到sigaction
函数。
void myfunc(int signum)
{
int i;
i = 0;
while (i < 5)
{
printf("process things signum %d\n", signum);
sleep(1);
i++;
}
return; //返回 main 函数
}
int main()
{
int i;
i = 0;
signal(14, myfunc); //14 就是 alarm 信号
printf("alarm before\n");
alarm(3);
printf("alarm after\n");
while (i < 5)
{
i++;
sleep(1);
printf("process things, i=%d\n", i);
}
return 0;
}
父子进程发送信号
void myfunc(int signum)
{
int i;
i = 0;
while (i < 2)
{
printf("process things signum %d\n", signum); //10
sleep(1);
i++;
}
return; //返回 main 函数
}
void myfunc1(int signum)
{
printf("receive %d\n", signum);
wait(NULL);
return; //返回 main 函数
}
void myfunc2(int signum)
{
printf("2receive %d\n", signum);
wait(NULL);
return; //返回 main 函数
}
int main()
{
int status = 0;
pid_t pid;
pid = fork(); //while((p1=fork( ))==-1); /*创建子进程p1*/
if (pid > 0)
{
//wait(NULL);如果想在这里回收子进程会产生阻塞
if (waitpid(pid, &status, WNOHANG) == 0) //成功返回子进程的pid
{
printf("收到exit status%d\n", status); //收到子进程的pid
printf("收到exit status %d\n", pid);
}
signal(10, myfunc); //14就是alarm信号
// signal(20, myfunc1); //17就是exit里面的child信号
// signal(SIGCHLD, myfunc2); //17就是exit里面的child信号
while (1)
{
sleep(1);
printf("I am father\n");
}
}
if (pid == 0)
{
// pause();
while (1)
;
// kill(getppid(), 10); //用户自定义信号 10
// sleep(5);
// exit(0); //里面包括了发送给父进程的kill(getppid(), SIG_CHLD) 17
}
return 0;
}
IPC通信---(Inter-Process Communicate)
同样都在内核空间,不在用户空间。
和文件I/O
很像。只是函数形式不一样。
- 共享内存
- 信号灯
- 消息队列
这个创建的【共享内存】,其实就是在【内核空间】创造的一块【数组缓存】
key 为
IPC_PRIVEAT
0
只能用在亲缘进程间通信。
ftok
创建的 key 可以用在无亲缘关系进程间通信。
a.c
、’a'
相当于创建一个相同key
的原料,可以理解为是一套相同的模具。
shmat shmdt 函数
#include
#include
int main()
{
int shmid;
int key;
char *p = NULL; //映射内核共享内存地址到用户空间
//不要打双引号,不然不能由char转化为int报警告:
// warning: incompatible pointer to integer conversion passing 'char [2]' to parameter of type 'int' [-Wint-conversion]
key = ftok("./hello.c", 'a'); //当前目录随便创建另一个文件拿来用
//key = ftok(".", 1); 这样就是将fname设为当前目录。
if (key < 0)
{
printf("create key failed\n");
return -1;
}
printf("create key succeeded key %d\n", key);
shmid = shmget(key, 128, IPC_CREAT | 0777);
if (shmid < 0)
{
printf("create share memory failed\n");
return -2;
}
printf("create share memory succeeded shmid=%d\n", shmid);
system("ipcs -m");
//NULL -- 系统自动分配 0 -- 可以读写
p = (char *)shmat(shmid, NULL, 0);
if (NULL == p)
{
printf("shmat failed\n");
return -3;
}
//操作这块映射到用户空间的内存
fgets(p, 128, stdin); //用键盘接收输入
printf("share memory data:%s", p);
//system("ipcrm -m shmid");
return 0;
}
其实命令 ipcs -m
就是函数,
自制 ipcrm
命令 :
#include
int main(int argc, char *argv[])
{
int shmid;
if (argc < 3)
{
printf("Please input param\n");
return -1;
}
if (strcmp(argv[1], "-m") == 0)
printf("delete share memory\n");
else
return -2;
shmid = atoi(argv[2]); //字符串转为 int
printf("shmid=%d", shmid);
shmctl(shmid, IPC_RMID, NULL); //删除内核里面的共享内存
//system("ipcrm -m shmid"); 相当于自制了这个
system("ipcs -m");
return 0;
}
同样的:
ipcs -m
其实就是用到了 shmcrl
里面第三个参数IPC_STAT
。
举个例子
仍然是父子间通信,父进程一直写入,子进程一直读取:
void myfun(int signum)
{
return;
}
int main(int argc, char *argv[])
{
pid_t pid;
int shmid; //创建一个内核共享内存,让父进程写,子进程读
char *p;
//必须先写到创建 子进程 之前,这样才能共享的是同一块共享内存,否则是各自操作各自的。
shmid = shmget(IPC_PRIVATE, 128, IPC_CREAT | 0777);
if (shmid < 0)
{
printf("create share memory failed\n");
return -1;
}
printf("create share memory ok. shmid=%d\n", shmid);
pid = fork();
if (pid < 0)
{
printf("create new process failed\n");
return -2;
}
if (pid > 0)
{
signal(SIGUSR2, myfun); //父进程接收子进程读完信号
p = (char *)shmat(shmid, NULL, 0); //自动分配内存,可读写
if (NULL == p)
{
printf("parent process:shmat function failed\n");
return -3;
}
while (1)
{
printf("parent process write start write in this shared memory:\n");
// scanf("%d", p);
fgets(p, 128, stdin); //标准输入
kill(pid, SIGUSR1); //父进程发送给子进程写完信号
pause(); //等待子进程读
}
}
if (0 == pid)
{
signal(SIGUSR1, myfun); //子进程接收父进程写完信号
p = (char *)shmat(shmid, NULL, 0); //自动分配内存,可读写
if (NULL == p)
{
printf("child process:shmat function failed\n");
return -3;
}
while (1)
{
pause(); //等待父进程写
printf("share memory data:%s", p);
kill(getppid(), SIGUSR2); //子进程发送读完信号
}
}
shmdt(p); //删除映射出来的共享内存
shmctl(shmid, IPC_RMID, NULL);
//或者写
// system("ipcrm -m shmid");
system("ipcs -m");
return 0;
}
再举个例子
【无亲缘】关系进程间通过【共享内存】通信 ---- 比如服务器端
和客户端
。
sever服务端
和client客户端
为非亲缘进程,server端
把自己的pid
写进共享内存,客户端
就知道是谁发过来的。(先读取上面写入的客户端pid再把自己的写进去)
同理客户端
也把自己的pid
写进去告诉sever端
它是谁。
client客户端:
struct mybuf
{
int pid;
char buf[124];
};
void myfun(int signum)
{
return;
}
int main()
{
int shmid;
int key;
struct mybuf *p;
int pid;
key = ftok("./hello.c", 'a');
if (key < 0)
{
printf("creat key failure\n");
return -1;
}
printf("creat key sucess\n");
shmid = shmget(key, 128, IPC_CREAT | 0777);
if (shmid < 0)
{
printf("creat share memory failure\n");
return -1;
}
printf("creat share memory sucess shmid=%d\n", shmid);
signal(SIGUSR1, myfun);
p = (struct mybuf *)shmat(shmid, NULL, 0);
if (p == NULL)
{
printf("parent process:shmat function failure\n");
return -3;
}
//client端 这儿开始和server不同
//get server pid
//pid是server端的,p->pid是客户端的
//read server share memory
pid = p->pid;
//write client pid to share memory
p->pid = getpid();
//kill signal 通知 server 端
kill(pid, SIGUSR2); //这里pid是server端的,
//client start read data from share memory
while (1)
{
pause(); //wait server write data to share memory;
printf("client process receve data from share memory:%s\n", p->buf); //read data
kill(pid, SIGUSR2); //服务器端读完了,server可以write share memory
}
// if (感觉要判断下如果 server端已经删除了,这边就不删了,否则会去删除一个不存在的p,而报错)
// {
// // shmdt(p);
// // shmctl(shmid, IPC_RMID, NULL);
// }
system("ipcs -m ");
return 0;
}
server服务端:
struct mybuf //总共是128个字节
{
int pid; //占4个
char buf[124];
};
void myfun(int signum)
{
return;
}
int main(int argc, char *argv[])
{
pid_t pid;
int shmid; //创建一个内核共享内存,让父进程写,子进程读
struct mybuf *p;
int key;
key = ftok("./hello.c", 'a'); //非亲子进程间不能用 IPC_PRIVATE
if (key < 0)
{
printf("create key failed\n");
return -4;
}
printf("creat key sucess\n");
shmid = shmget(key, 128, IPC_CREAT | 0777);
if (shmid < 0)
{
printf("create share memory failed\n");
return -1;
}
printf("create share memory ok. shmid=%d\n", shmid);
signal(SIGUSR2, myfun); //SIGUSR2是客户端发来的
p = (struct mybuf *)shmat(shmid, NULL, 0); //自动分配内存,可读写
if (NULL == p)
{
printf("parent process:shmat function failed\n");
return -3;
}
//get client pid
p->pid = getpid(); //write server pid to share memory
pause(); //wait client read server pid;
pid = p->pid; //读客户端又写进来的pid
//写入共享内存自己的pid
while (1)
{
printf("server process start to write share memory:\n");
fgets(p->buf, 128, stdin);
kill(pid, SIGUSR1); // client process read data
pause(); // wait client process read
}
shmdt(p); //删除映射出来的共享内存
shmctl(shmid, IPC_RMID, NULL);
system("ipcs -m");
return 0;
}
消息队列 和 管道 的区别:
管道
也是队列形式存在,而且是一个顺序队列 ( 数组实现的队列 ) 。
消息队列 和 共享内存 的区别:
消息队列
的创建不需要像创建共享内存
那样定义大小,因为它是队列
可以不断插入新元素。
struct msgbuf
{
long type;
char voltage[124];
char ID[4];
};
int main(int argc, char *argv[])
{
int msgid;
int readreturn; //读出来的返回值
struct msgbuf sendbuf, recvbuf;
msgid = msgget(IPC_PRIVATE, 0777);
if (msgid < 0)
{
printf("create message queue failed\n");
return -1;
}
printf("create message queue success msgid=%d\n", msgid);
system("ipcs -q");
//init sendbuf
sendbuf.type = 100; //相当于一个查找的标签
printf("please input message:\n");
fgets(sendbuf.voltage, 124, stdin);
//write
msgsnd(msgid, (void *)&sendbuf, strlen(sendbuf.voltage), 0); //阻塞
//read
memset(sendbuf.voltage, 0, 124); //清除缓存,这124大小都写0
readreturn = msgrcv(msgid, (void *)&recvbuf, 124, 100, 0); //以阻塞方式读recvbuf里面124个类型为100的值
printf("recv:%s", recvbuf.voltage);
printf("readreturn:%d", readreturn);
//delete
msgctl(msgid, IPC_RMID, NULL);
system("ipcs -q");
return 0;
}
msgsnd()
--发送。因为是队列,相当于队列的插入。
msgrcv()
--接收。相当于队列的删除。所以读完之后就没有了!
实现无亲缘关系的进程间如何进行通信
消息队列
不像 共享内存
的流程,它不需要信号通知。
消息队列
的另一个特点:信息读取完自动删除;可以阻塞
例一:两个非亲缘线程单向通信,一个发送,一个读取。
例二:两个非亲缘线程单向通信,双向通信,类似于聊天工具。
详细代码:https://blog.csdn.net/wenwen111111/article/details/54705816
信号灯Semaphore
信号量的集合,多个信号量。
#include "sys/sem.h"
#include "signal.h"
#include "unistd.h"
#include
#include
int main()
{
int semid;
semid=semget(IPC_PRIVATE,3,0777);//建立三个信号
if(semid <0)
{
printf("creat semaphore failure\n");
return -1;
}
printf("creat semaphore sucess semid=%d\n",semid);
system("ipcs -s");
//while(1);
// delete semaphore
//删除信号灯里面的第一个(0), 第四个参数可以NULL也可以不写
semctl(semid,0,IPC_RMID,NULL);
system("ipcs -s");
return 0;
}
posix标准:(相当于它是黄金,通用的货币 )
完成同一功能,不同内核提供的系统调用(也就是一个函数)是不同的,
例如:
创建进程,linux
下是 fork 函数,windows
下是 creatprocess 函数。好,我现在在linux
下写一个程序,用到 fork 函数,那么这个程序该怎么往windows
上移植?我需要把源代码里的 fork 通通改成 creatprocess,然后重新编译...
posix
标准的出现就是为了解决这个问题。linux
和windows
都要实现基本的posix
标准,linux
把 fork 函数封装成 posix_fork(随便说的),windows
把 creatprocess 函数也封装成 posix_fork,都声明在unistd.h里。这样,程序员编写普通应用时候,只用包含 unistd.h,调用 posix_fork 函数,程序就在源代码级别可移植了。
进程 线程
早期没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程。
进程是获取资源 (分配资源的最小单位
),线程是使用资源(程序执行的最小单位
)。线程是进程的实体。
一些基本概念
并发: 在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。(看起来同时发生,假象;针对单核CPU)
并行:在同一时刻,有多条指令在多个处理器上同时执行。(正在的同时发生)
同步:彼此有依赖关系的调用不应该“同时发生”,而同步就是要阻止那些“同时发生”的事情。(我们采取同步这种方式让有些事情不要同时发生----比如数据库一个锁 就是防止让 add 和 delete 同时发生)
异步:与同步相对,任何两个彼此独立的操作是异步的,它表明事情独立的发生。
多线程不一定要多处理器。
线程编译要加一个库 gcc -lpthread 文件.c -0 文件
。
LPTHREAD
指向一个函数,该函数通知宿主某个线程已开始执行。
pthread(POSIX thread),简称为pthread,是线程的POSIX标准,在类Unix操作系统中(Unix、Linux、Mac OS X等),都是用pthread作为操作系统的线程。作为其编程标准的头文件。
注意 ⚠️ :线程安全是gcc -pthread
编译;线程非安全是gcc-lpthread
编译
#include
void print_id(char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid is %u, tid is 0x%x\n", s, pid, tid);
}
void *thread_fun(void *arg)
{
print_id(arg);
return (void *)0; //返回一个 0 空指针(万能指针)需要进行强制类型转换
}
int main(int argc, char **argv)
{
pthread_t ntid;
int err;
err = pthread_create(&ntid, NULL, thread_fun, "new thread");
if (err != 0)
{
printf("create new thread failed: %d\n", err);
return 0;
}
print_id("main thread:");
sleep(2); //主线程睡两秒,等待线程的顺利执行
return 0;
}
主线程 main()
使用结构体:
typedef struct student
{
int age; /* Pointer to actual object */
char name[20]; /* Extra information - reuse count etc */
} STUDENT;
void *thread_fun(void *stu)
{
//传过来是 void * stu万能指针,必须转回结构体指针然后指向结构体成员
printf("student age is %d, name is %s\n", ((STUDENT *)stu)->age, ((STUDENT *)stu)->name);
return (void *)0; //返回一个 0 空指针(万能指针)需要进行强制类型转换
}
int main(int argc, char **argv)
{
pthread_t tid;
int err;
STUDENT student;
student.age = 100;
memcpy(student.name, "张三", 20);
// strcpy(student.name, "张三");
err = pthread_create(&tid, NULL, thread_fun, (void *)&student);
if (err != 0)
{
printf("create new thread failed: %d\n", err);
return 0;
}
int i;
for (i = 0; i < argc; i++)
{
printf("main thread args: %s\n", argv[i]);
}
sleep(1);
return 0;
}
如果主线程先 over 了,其他线程就打印不出来:
(只把 sleep 移动一下位子,让主线程先结束)
typedef struct student
{
int age;
char name[20];
} STUDENT;
void *thread_fun(void *stu)
{
sleep(1);
printf("student age is %d, name is %s\n", ((STUDENT *)stu)->age, ((STUDENT *)stu)->name);
return (void *)0; /
}
int main(int argc, char **argv)
{
pthread_t tid;
int err;
STUDENT student;
student.age = 100;
memcpy(student.name, "张三", 20);
// strcpy(student.name, "张三");
err = pthread_create(&tid, NULL, thread_fun, (void *)&student);
if (err != 0)
{
printf("create new thread failed: %d\n", err);
return 0;
}
int i;
for (i = 0; i < argc; i++)
{
printf("main thread args: %s\n", argv[i]);
}
//sleep(1); 只把这个 sleep 弄到子线程了
return 0;
}
线程的四种状态
僵尸线程: 一个没有被分离的线程在终止时会保留它的虚拟内存,包括他们的堆栈和其他系统资源。
线程默认创建的时候是非分离的。
如果线程具有分离属性,线程终止时会被立刻回收♻️ ,回收将释放掉所有在线程终止时未释放的系统资源和进程资源,包括保存线程返回值的内存空间、堆栈、保存寄存器的内存空间等。
注意 ⚠️ :必须手动释放由该线程占有的程序资源。由 malloc 或者 mmap 分配的内存可以在任何时候由任何线程释放,条件变量、互斥量、信号灯 可以由任何线程销毁,只要他们被解锁了 或者没有线程等待。互斥量只能主人才能解锁,所以在线程终止前,必须解锁互斥量。
线程的终止
exit 是危险的。
如果进程中的任意一个线程调用了exit
, _Exit
,_exit
,那么整个进程会终止。
pthread_join()
线程的合并是一种主动回收线程资源的方案。当一个进程或线程调用了针对其它线程的pthread_join()
接口,就是线程合并了。这个接口会阻塞调用进程或线程,直到被合并的线程结束为止。当被合并线程结束,pthread_join()
接口就会回收这个线程的资源,并将这个线程的返回值返回给合并者。
对线程而言,同样有类似于“僵线程 ☠️ ”的概念。比如,主线程(或任何其他一个线程)没有对即将结束的子线程调用 pthread_join()
函数,当该线程结束之后就会处于僵线程状态。
僵线程状态下的线程同样面临着一个问题:线程已经结束,但是线程特有的资源(如线程ID,栈空间等)却没有释放。
pthread_detach()
虽然 pthread_join()
解决了僵线程的问题,但同时也带来了主线程阻塞的附加问题。那么,有没有一种既不会让从线程沦为僵线程,也不会让主线程阻塞的方案呢?
将线程设置为分离状态,可以在避免僵线程的同时不会阻塞主线程。设置线程分离有两种方法:
① 种是在创建线程时设置线程属性为分离的;
② 另一种是调用 pthread_detach()
函数完成线程分离属性的设置
线程分离是将线程资源的回收 ♻️ 工作交由系统自动来完成,也就是说当被分离的线程结束之后,系统会自动回收它的资源。因为线程分离是启动系统的自动回收机制,那么程序也就无法获得被分离线程的返回值,这就使得pthread_detach()
接口只要拥有一个参数就行了,那就是被分离线程句柄。
线程合并和线程分离都是用于回收线程资源的,可以根据不同的业务场景酌情使用。不管有什么理由,你都必须选择其中一种,否则就会引发资源泄漏的问题,这个问题与内存泄漏同样可怕 。
pthread_cancel()
pthread_kill() 发送信号
void *thread_fun(void *arg)
{
// sleep(1);
printf("child thread\n");
return (void *)0;
}
int main(int argc, char **argv)
{
pthread_t tid;
int err;
int s;
void *rval;
err = pthread_create(&tid, NULL, thread_fun, NULL);
if (err != 0)
{
printf("create thread failed\n");
return 0;
}
// sleep(1);
s = pthread_kill(tid, SIGQUIT);
if (ESRCH == s)
{
printf("thread tid is not found\n");
}
pthread_join(tid, &rval);//
printf("main thread finished\n");
return 0;
}
sigprocmask
是进程里面屏蔽信号函数。
pthread_sigmask
是线程里面屏蔽信号函数。
使用一下 sigaction()
来处理信号。
struct sigaction
{
void (*sa_handler)(int signum) ;/*旧的信号处理函数指针*/
void (*sa_sigaction)(int signum, siginfo_t *info, void *context);/*新的信号处理函数指针*/
sigset_t sa_mask;/*信号阻塞集*/
int sa_flags;/*信号处理的方式*/
};
用sa_handler
指向我们的一个信号操作函数,就可以了。sa_handler有两个特殊的值:SIG_DEL
和SIG_IGN
。SIG_DEL是使用缺省的信号操作函数,而SIG_IGN是使用忽略该信号的操作函数。
void sig_handler1(int arg)
{
printf("thread1 get signal:%d\n", arg);
return;
}
void *thread_fun(void *arg)
{
struct sigaction act;
printf("child thread created\n");
memset(&act, 0, sizeof(act)); //sigemptyset函数初始化信号集合set,将set设置为空。sigfillset也初始化信号集合,只是将信号集合设置为所有信号的集合
sigaddset(&act.sa_mask, SIGQUIT); //添加一个信号到信号集
act.sa_handler = sig_handler1; //指定信号处理回调函数
sigaction(SIGQUIT, &act, NULL); //收到信号然后处理
pthread_sigmask(SIG_BLOCK, &act.sa_mask, NULL); //屏蔽信号,这样发送一个SIGQUIT也不会退出
//SIG_BLOCK:增加一个信号集合到当前进程的阻塞集合之中。
//SIG_UNBLOCK:从当前的阻塞集合之中删除一个信号集合。
//SIG_SETMASK:将当前的信号集合设置为信号阻塞集合。
printf("child thread sleep begin\n");
sleep(5);
printf("child thread sleep finished\n");
return (void *)0;
}
int main(int argc, char **argv)
{
pthread_t tid;
int err;
int s;
void *rval;
err = pthread_create(&tid, NULL, thread_fun, NULL);
if (err != 0)
{
printf("create thread failed\n");
return 0;
}
sleep(1); //给时间让thread建立
printf("before send a signal\n");
s = pthread_kill(tid, SIGQUIT); //发信号给thread
printf("after send a signal\n");
if (ESRCH == s)
{
printf("thread tid is not found\n");
}
pthread_join(tid, &rval); //会在这儿卡5秒钟,一直等thread里面睡完退出
printf("main thread finished\n");
return 0;
}
一般来说,Posix 的线程终止有两种情况:正常终止和非正常终止。线程主动调用 pthread_exit()
或者从线程函数中 return
都将使线程正常退出,这是可预见的退出方式;
非正常终止是线程在其他线程的干预下,或者由于自身运行出错(比如访问非法地址)而退出,这种退出方式是不可预见的。
pthread_cleanup_push()/pthread_cleanup_pop()的详解
void clean_fun1(void *arg)
{
printf("this is clean fun1\n");
}
void clean_fun2(void *arg)
{
printf("this is clean fun2\n");
}
void *thread_fun(void *arg)
{
pthread_cleanup_push(clean_fun1, NULL);
pthread_cleanup_push(clean_fun2, NULL);
sleep(5);
//这里要注意,如果将sleep(100);换成while(1);的话,程序会一直暂停.push和pop要成对出现.
//因为while(1);运行的太快,线程不接受cancel信号
//while(1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return NULL;
}
int main()
{
pthread_t tid1;
int err;
err = pthread_create(&tid1, NULL, thread_fun, NULL);
if (err != 0)
{
perror("pthread_create");
exit(0);
}
sleep(3);
//printf("test\n");
err = pthread_cancel(tid1);
if (err != 0)
{
perror("cancel error:");
exit(0);
}
err = pthread_join(tid1, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
return 0;
}
线程的同步
互斥量
struct student
{
int id;
int age;
} student;
int i;
void *thread_fun1(void *arg)
{
while (1)
{
student.id = i;
student.age = i;
i++;
if (student.id != student.age)
{
printf("%d,%d\n", student.id, student.age);
break;
}
}
return (void *)0;
}
void *thread_fun2(void *arg)
{
while (1)
{
student.id = i;
student.age = i;
if (student.id != student.age)
{
printf("%d,%d\n", student.id, student.age);
break;
}
i++;
}
return (void *)0;
}
int main()
{
pthread_t tid1, tid2;
int err;
err = pthread_create(&tid1, NULL, thread_fun1, NULL);
if (err != 0)
{
perror("pthread_create");
exit(0);
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if (err != 0)
{
perror("pthread_create");
exit(0);
}
sleep(3);
err = pthread_join(tid1, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
err = pthread_join(tid2, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
return 0;
}
对上面程序改造,加锁 之后,就不会出现以上两个不同线程改变了同一全局变量,只有拿到 锁才能去改变。
struct student
{
int id;
int age;
} student;
int i;
pthread_mutex_t mutex; //这也是一个结构体
void *thread_fun1(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex); //加锁
student.id = i;
student.age = i;
i++;
if (student.id != student.age)
{
printf("fun1:%d,%d\n", student.id, student.age);
break;
}
pthread_mutex_unlock(&mutex); //
}
return (void *)0;
}
void *thread_fun2(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex); //加锁
student.id = i;
student.age = i;
i++;
if (student.id != student.age)
{
printf("fun2:%d,%d\n", student.id, student.age);
break;
}
pthread_mutex_unlock(&mutex); //
}
return (void *)0;
}
int main()
{
pthread_t tid1, tid2;
int err;
//对互斥量进行初始化,只有初始化过互斥量才能使用
err = pthread_mutex_init(&mutex, NULL);
if (err)
{
printf("init rwlock failed\n");
return err;
} err = pthread_create(&tid1, NULL, thread_fun1, NULL);
if (err != 0)
{
perror("pthread_create");
exit(0);
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if (err != 0)
{
perror("pthread_create");
exit(0);
}
sleep(3);
//等待新线程结束
err = pthread_join(tid1, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
err = pthread_join(tid2, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
pthread_mutex_destroy(&mutex);
return 0;
}
读写锁 ✏️
读锁又称为共享锁,如果进程1对一个数据上了读锁,进程1只能读,其他进程也只能对这个数据上读锁;
写锁又叫排他锁,如果进程1给数据上写锁,则进程1可以对该数据进行读写,而其他的进程不能对该数据操作。
证明多个线程可以同时有读锁 :
int num = 0;
pthread_rwlock_t rwlock; //读锁
void *thread_fun1(void *arg)
{
pthread_rwlock_rdlock(&rwlock); //加载读
printf("thread 1 print num %d\n", num);
sleep(5); //休息时跑到主线程,主线程是调用锁,阻塞状态
printf("thread 1 sleep over\n");
pthread_rwlock_unlock(&rwlock); //解锁 成对出现
return (void *)1;
}
void *thread_fun2(void *arg)
{
pthread_rwlock_rdlock(&rwlock); //加载读
printf("thread 2 print num %d\n", num);
sleep(5);
printf("thread 2 sleep over\n");
pthread_rwlock_unlock(&rwlock); //解锁 成对出现
return (void *)2;
}
int main()
{
pthread_t tid1, tid2;
int err;
//对读锁进行初始化,只有初始化过才能使用
err = pthread_rwlock_init(&rwlock, NULL);
if (err)
{
printf("init rwlock failed\n");
return err;
}
err = pthread_create(&tid1, NULL, thread_fun1, NULL);
if (err != 0)
{
perror("pthread 1 create error\n");
exit(0);
}
err = pthread_create(&tid2, NULL, thread_fun2, NULL);
if (err != 0)
{
perror("pthread 2 create error\n");
exit(0);
}
sleep(3);
//等待新线程结束
err = pthread_join(tid1, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
err = pthread_join(tid2, NULL);
if (err != 0)
{
perror("pthread_join error:");
exit(0);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
证明多个线程只能有一个可以用写锁 :
一个读锁,一个写锁,会怎样? :
另一个例子:
int count = 0;
pthread_rwlock_t rwlock;
void *route_read(void *arg)
{
int i = *(int *)arg;
free(arg);
while (1)
{
pthread_rwlock_rdlock(&rwlock);
printf("%d read %d\n", i, count);
pthread_rwlock_unlock(&rwlock);
usleep(1000);
}
}
void *route_write(void *arg)
{
int i = *(int *)arg;
free(arg);
while (1)
{
pthread_rwlock_wrlock(&rwlock);
printf("%d write %d\n", i, ++count);
pthread_rwlock_unlock(&rwlock);
usleep(1000);
}
}
int main()
{
pthread_t id[8];
int i = 0;
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 5; i++)
{
int *p = (int *)malloc(sizeof(int));
*p = i;
pthread_create(&id[i], NULL, route_read, (void *)p);
}
for (i = 5; i < 8; i++)
{
int *p = (int *)malloc(sizeof(int));
*p = i;
pthread_create(&id[i], NULL, route_write, (void *)p);
}
for (i = 0; i < 8; i++)
{
pthread_join(id[i], NULL);
}
pthread_rwlock_destroy(&rwlock);
}
条件变量
视频例子参考这里。
另外一个人的例子 感觉更能理解 :
#define BUFFER_SIZE 5
pthread_mutex_t g_mutex;
pthread_cond_t g_cond;
typedef struct {
char buf[BUFFER_SIZE];
int count;
} buffer_t;
buffer_t g_share = {"", 0};
char g_ch = 'A';
void* producer( void *arg )
{
printf( "Producer starting.\n" );
while( g_ch != 'Z' ) {
pthread_mutex_lock( &g_mutex );
if( g_share.count < BUFFER_SIZE ) {
g_share.buf[g_share.count++] = g_ch++;
printf( "Prodcuer got char[%c]\n", g_ch - 1 );
if( BUFFER_SIZE == g_share.count ) {
printf( "Producer signaling full.\n" );
pthread_cond_signal( &g_cond );
}
}
pthread_mutex_unlock( &g_mutex );
}
printf( "Producer exit.\n" );
return NULL;
}
void* consumer( void *arg )
{
int i;
printf( "Consumer starting.\n" );
while( g_ch != 'Z' ) {
pthread_mutex_lock( &g_mutex );
printf( "Consumer waiting\n" );
pthread_cond_wait( &g_cond, &g_mutex );
printf( "Consumer writing buffer\n" );
for( i = 0; g_share.buf[i] && g_share.count; ++i ) {
putchar( g_share.buf[i] );
--g_share.count;
}
putchar('\n');
pthread_mutex_unlock( &g_mutex );
}
printf( "Consumer exit.\n" );
return NULL;
}
int main( int argc, char *argv[] )
{
pthread_t ppth, cpth;
pthread_mutex_init( &g_mutex, NULL );
pthread_cond_init( &g_cond, NULL );
pthread_create( &cpth, NULL, consumer, NULL );
pthread_create( &ppth, NULL, producer, NULL );
pthread_join( ppth, NULL );
pthread_join( cpth, NULL );
pthread_mutex_destroy( &g_mutex );
pthread_cond_destroy( &g_cond );
return 0;
}
线程的高级属性
1. 一次性初始化:
互斥量智能初始化一次,再次初始化会出现错误。比如自己写库函数时将互斥量封装进去,这时一次性初始化就用上场了。
定义变量pthread_once_t once_control = PTHREAD_ONCE_INIT
;
2. 线程属性
创建线程时可以使用pthread_attr_t
类型参数变更属性。
默认创建的线程是非分离的。
创建一个一开始就分离的线程例子
修改线程的栈大小与地址(视频)
3. 线程的同步属性
视频教材
父子进程直接操作一块共享内存,但是各自加一个互斥锁。
(用条件变量,读写锁一样加锁,有不同的调用 api
)
如果不加锁 的情况 :
加锁来改造 :
int main()
{
char *shm = "myshm";
char *shm1 = "myshm1";
int shm_id, shm_id1;
char *buf;
pid_t pid;
pthread_mutex_t *mutex;
pthread_mutexattr_t mutexattr;
//打开共享内存
shm_id1 = shm_open(shm1, O_RDWR | O_CREAT, 0644);
//调整共享内存大小
ftruncate(shm_id1, 100);
//映射共享内存,MAP_SHARED属性表明,对共享内存的任何修改都会影响其他进程
mutex = (pthread_mutex_t *)mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, shm_id1, 0);
pthread_mutexattr_init(&mutexattr);
#ifdef _POSIX_THREAD_PROCESS_SHARED
pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED);
#endif
pthread_mutex_init(mutex, &mutexattr);
//打开共享内存
shm_id = shm_open(shm, O_RDWR | O_CREAT, 0644);
//调整共享内存大小
ftruncate(shm_id, 100);
//映射共享内存,MAP_SHARED属性表明,对共享内存的任何修改都会影响其他进程
buf = (char *)mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, shm_id, 0);
pid = fork();
if (pid == 0)
{
//休眠1s,让父进程先运行
sleep(1);
printf("I'm child proccess\n");
pthread_mutex_lock(mutex);
//将共享内存内存修改为hello
memcpy(buf, "hello", 6);
printf("child buf is : %s\n", buf);
pthread_mutex_unlock(mutex);
}
else if (pid > 0)
{
printf("I'm parent proccess\n");
pthread_mutex_lock(mutex);
//修改共享内存到内容,改为world
memcpy(buf, "world", 6);
sleep(3);
printf("parent buf is : %s\n", buf);
pthread_mutex_unlock(mutex);
}
pthread_mutexattr_destroy(&mutexattr);
pthread_mutex_destroy(mutex);
//解除映射
munmap(buf, 100);
//消除共享内存
shm_unlink(shm);
//解除映射
munmap(mutex, 100);
//消除共享内存
shm_unlink(shm1);
}
4. 线程私有数据
线程局部变量,线程的其他函数难以访问, 全局变量可以解决问题(有弊端)。
因此引入TLS
概念(不同线程有独立的存储空间
)。
(C程序库中的
errno
是个最典型的一个例子。errno是一个全局变量,会保存最后一个系统调用的错误代码。在单线程环境并不会出现什么问题。但是在多线程环境,由于所有线程都会有可能修改errno,这就很难确定errno代表的到底是哪个系统调用的错误代码了。这就是有名的“非线程安全(Non Thread-Safe)”的。)
#define THREAD_COUNT 10
pthread_key_t g_key;
typedef struct thread_data
{
int thread_no;
} thread_data_t;
void show_thread_data()
{
thread_data_t *data = pthread_getspecific(g_key);
printf("Thread %d \n", data->thread_no);
}
void *thread(void *arg)
{
thread_data_t *data = (thread_data_t *)arg;
printf("Start thread %d\n", data->thread_no);
pthread_setspecific(g_key, data);
show_thread_data();
printf("Thread %d exit\n", data->thread_no);
return (void *)0;
}
void free_thread_data(void *arg)
{
thread_data_t *data = (thread_data_t *)arg;
printf("Free thread %d data\n", data->thread_no);
free(data);
}
int main(int argc, char *argv[])
{
int i;
pthread_t pth[THREAD_COUNT];
thread_data_t *data = NULL;
pthread_key_create(&g_key, free_thread_data);//第二个参数是一个析构函数
for (i = 0; i < THREAD_COUNT; ++i)
{
data = malloc(sizeof(thread_data_t));
data->thread_no = i;
pthread_create(&pth[i], NULL, thread, data);//将data结构体传到thread线程函数里
}
for (i = 0; i < THREAD_COUNT; ++i)
pthread_join(pth[i], NULL);
pthread_key_delete(g_key);
return 0;
}
视频例子参考这里。
再来个例子 :
pthread_key_t key;
void *thread_fun1(void *arg)
{
printf("thread 1 start!\n");
int a = 1;
//将a和key关联
pthread_setspecific(key, (void *)a);
sleep(2);
printf("thread 1 key->data is %d\n", pthread_getspecific(key));
return (void *)0;
}
void *thread_fun2(void *arg)
{
sleep(1);
printf("thread 2 start!\n");
int a = 2;
//将a和key关联
pthread_setspecific(key, (void *)a);
printf("thread 2 key->data is %d\n", pthread_getspecific(key));
return (void *)0;
}
int main()
{
pthread_t tid1, tid2;
//创造一个key
pthread_key_create(&key, NULL);
//创造新线程
if (pthread_create(&tid1, NULL, thread_fun1, NULL))
{
printf("create new thread 1 failed\n");
return -1;
}
if (pthread_create(&tid2, NULL, thread_fun2, NULL))
{
printf("create new thread 2 failed\n");
return -2;
}
//等待新线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_key_delete(key);
return 0;
}
5. 安全的 fork,避免死锁 deadlock
用 pthread_atfork()
改造:
Linux线程退出、资源回收、资源清理的方法
PS:对前面内容的一些总结,这人写的很好,点击上面链接 。
补充:
自旋锁
自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。
自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。
因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。由于这个原因,操作系统的内核经常使用自旋锁。
综合例子 :
socket() 创造套接字函数
然后设置 bind() ,调整 IP 参数:
然后设置 listen() :
然后设置 accept() :
收发数据 recv(), send()/sendto(), read(), write() :
最后关闭 close() :
因为本身 socket
是一种文件描述符,所以最后要关闭打开的文件 。