一、系统编程
系统内部多任务编程,通俗的理解就是拿一块CPU给他进行时间划分,让一个指令处理单元并发执行多个程序逻辑的操作。
多任务架构分为进程和线程两大载体。进程被称为资源的最小单位,而线程被称为调度的最小单位。资源的最小单位是指系统给应用程序划分资源的时候是以进程为单位区划分的,而调度的最小单位是指系统去调度指令是以线程为单位调度的。
一般执行程序的时候是先诞生进程,用进程申请资源,并运行一条线程给处理器进行指令调度,当然,每个进程里是可以有多条线程的。
二、进程
1、定义
资源的最小单位
2、包含资源
文件描述符、信号、vm(虚拟内存)……
3、现实应用场景
手机的微信、QQ、抖音、小红书同时运行,但各个程序都是独立的。
4、特点
每个进程有独立的资源;
每个进程的操作互不干扰;
系统申请独立的资源是以进程为单位。
5、应用场景
调度他人程序(例:用一个程序打开另外一个程序)
后台服务程序(守护进程/精灵进程)
6、进程间通信
异步信号
管道
system V IPC通信机制(共享内存、消息队列、信号量)
unix本地套接字
7、代码
(1)CP大法
通过 fork 函数创建进程,新创建的进程相当于一个孩子,它是原来的进程的复制粘贴版
创建子进程成功:会将“0”返回给子进程,子进程的PID返回给父进程;
创建子进程失败:返回“-1”,子进程不会被创建。
例如以下代码:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
fork(); //创建子进程printf("pooh\n");
return 0;
}
运行结果:
pooh //父进程
pooh //子进程
(2)父进程和子进程资源相互独立
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid; //创建子进程的PID(相当于身份证ID)int x;
pid = fork();//进入子进程
x = 32;
if(pid == 0)
{
sleep(1);
printf("仔, x =%d\n", x);
exit(EXIT_SUCCESS); //1代表失败,0代表成功,所以这里也可以写成exit(0);
}
printf("爹地\n");
x = 1128;
return 0;
}
运行结果:
爹地
仔, x = 32
(3)退出进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait>
int main(void)
{
pid_t pid;int x;
pid = fork();
x = 32;
if(pid == 0)
{
sleep(1);
printf("仔, x = %d\n", x);
exit(0); //退出进程
}
int status; //定义状态值
//pid_t wpid;
//wpid = wait(&status); //阻塞,等待子进程退出
printf("爹地\n");
x = 1128;
//printf("pid = %d, wpid = %d, status = %d\n", pid, wpid, status);
printf("pid = %d, status = %d\n", pid, WEXITSTATUS(status));
return 0;
}
运行结果:
仔, x = 32
爹地
pid = 4737, waitpid = 4737, status = 0
从(3)的运行结果上看,添加了 wait 函数之后对比起(2)的运行结果,父进程需要先等待一秒,等子进程输出完退出之后父进程才输出,在这里,被创建的子进程 PID 为 4737,接收到的子进程 WAITPID 同样为 4737,status 是接收子进程退出的返回状态值,这里 exit 函数里的值为 0,status 的值为 0。其逻辑是,status会将这个值(这里是 0) &0xff(即后八位为1,其余皆为0)。例如:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait>
int main(void)
{
pid_t pid;int x;
pid = fork();
x = 32;
if(pid == 0)
{
sleep(1);
printf("仔, x = %d\n", x);
exit(1); //退出进程
}
int status; //定义状态值
pid_t wpid;
wpid = waitpid(pid, &status, 0); //阻塞,等待指定子进程退出pid_t waitpid(pid, int *wstatus, int options); 这里 options 为 0 代表正常阻塞等待子进程退出,除此之外 options还有另外三种情况
printf("爹地\n");
x = 1128;
printf("pid = %d, waitpid = %d, status = %d\n", pid, waitpid, status);
//printf("%d", WEXITSTATUS(status)); //准确的来讲应该这么写才是获取状态退出值
return 0;
}
运行结果:
仔, x = 32
爹地
pid = 4737, waitpid = 4737, status = 256
这里就可以很明显的看到 exit(1); 返回的状态值为256,这里的 1 只是代表系统的状态,这里他代表的系统内部32位数据值,他需要 &0xff,那就只保留 8 位给到返回值 status,剩下 24 位是给系统预留的。
pid > 0 | 等待pid子进程退出 |
pid = 0 | 等待该调用者进程的任意一个子进程(同一进程组) |
pid = -1 | 等待任意的一个子进程 |
pid < -1 | 等待pid这个数值的绝对值所对应的进程组ID内的子进程 |
宏 | 含义 |
WIFEXITED(status) | 如果子进程正常退出,则该宏为真 |
WEXITSTATUS(status) | 如果子进程正常退出,则该宏将获取子进程的退出值 |
WIFSIGNALED(status) | 如果子进程被信号杀死,则该宏为真 |
WIERMSIG(status) | 如果子进程被信号杀死,则该宏获取导致他死亡的信号值 |
WCOREDUMP(status) | 如果子进程被信号杀死且生成核心转储文件(core dump),则该宏为真 |
WIFSTOPPED(status) | 如果子进程的信号被暂停,且option中WUNTRACED已被设置时,则该宏为真 |
WSTOPSIG(status) | 如果WIFSTOPPED(status)为真,则该宏将获取导致子进程暂停的信号值 |
WIFCONTINUED(status) | 如果子进程被信号SIGCONT重新置为就绪态,该宏为真 |
WCONTINUED | 报告任一从暂停状态出来且从未报告过的子进程的状态 |
WNOHANG | 非阻塞等待 |
WUNTRACED | 报告任一当前处于暂停状态且从未报告过的子进程的状态 |
(4)注册和退出进程
#include <stdio.h>
#include <stdlib.h> //atexit(); 需要
#include <unistd.h>
void func1(void)
{
printf("Pooh\n");
}
void func2(void)
{
printf("the\n");
}
void func3(void)
{
printf("Winnie\n");
}
void my_on_exit(int status, void *arg)
{
printf("status=%d, arg=%s\n", status, (char *)arg);
}
int main(void)
{
atexit(func1); //注册,当进程退出的时候要去运行什么内容
atexit(func2);
atexit(func3);
on_exit(my_on_exit, "世界上最可爱的小熊维尼"); //注册,当前进程退出的时候要运行什么内容(复杂版)
printf("Oh\n");
return 0;//仅代表main函数结束,去执行一段特殊代码,然后再清空缓冲区
}
运行结果:
Oh
status=0,arg=世界上最可爱的小熊维尼
Winnie
the
Pooh
(5)直接退出进程
#include <stdio.h>
#include <stdlib.h> //atexit(); on_exit();需要
#include <unistd.h>
void func1(void)
{
printf("Pooh\n");
}
void func2(void)
{
printf("the\n");
}
void func3(void)
{
printf("Winnie\n");
}
void my_on_exit(int status, void *arg)
{
printf("status=%d, arg=%s\n", status, (char *)arg);
}
int main(void)
{
atexit(func1); //注册,当进程退出的时候要去运行什么内容
atexit(func2);
atexit(func3);
on_exit(my_on_exit, "世界上最可爱的小熊维尼"); //注册,当前进程退出的时候要运行什么内容(复杂版)
printf("Oh\n");
_exit(0); //直接退出
return 0;//仅代表main函数结束,去执行一段特殊代码,然后再清空缓冲区
}
运行结果:无,程序已经直接退出来了
(6)获取进程ID
getpid | 获取当前程序的进程ID |
getppid | 获取当前程序的父进程ID |
getpgid(pid) | 获取 pid 这个进程的组 ID |
setpgrp | 设置进程组ID 为自己进程的ID |
(7)进程的调用
1)调度第三方程序
1.1)exec系列函数(执行系列函数)
int execl(const char *path, const char *arg, .../*(char *)NULL */);
int execlp(const char *file, const char *arg, .../*(char *)NULL */);
int execv(const char *path, char *const arg[ ]);
int execvp(const char *file, char *const arg[ ]);
清空资源之后再运行程序(内存资源和指定的文件资源)
被保留:没有指定的文件资源,子进程的ID与设置
1.2)vfork
不复制父进程内存资源去创建子进程,但子进程去操作父进程的内容,父进程会进入睡眠状态,只有当vfork创建出来的子进程调用了exec系列函数,去加载第三方程序的时候或者是子进程结束,父进程才会被唤醒。
使用 fork 和 vfork 功能都一样,只是效率不同,若需要操作父进程的资源或者创建子进程的耗时短则用 vfork 就可以了,但是如果创建子进程的耗时比较长用 fork 的效率会高点。
1.3)守护进程/精灵进程
让一个程序一旦运行就可以脱离终端的控制,变成一个在后台默默运行的服务。
操作过程:
忽略终端的挂断信号;
新建子进程,退出父进程,脱离回话的管理;
新建会话,脱离原本会话;
新建子进程,退出父进程,脱离新会话的管理;
新建进程组,让程序脱离原本的进程组;
关闭所有文件描述符;
改变工作路径到根目录;
更改掩码。
(8)进程间的通信方式
1)管道通信
由Linux系统内部提供一种通信方式,相当于在内核空间开辟一块类似缓冲区的区域,可以往这片区域写入或读取内容。适用于单对单的流通信。
1.1)无名/匿名管道通信 pipe
特点:
没有名字;
只能用直系亲属关系的进程中使用,子进程靠继承父进程的文件描述符才能使两个进程操作在同一个管道;
半双工通信,要么读、要么写;
写入数据没有原子性;
具备阻塞特性(无数据 / 管道缓冲区已满);
无法用 lseek 定位
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
pid_t pid;int pfd[2];
pipe(pfd); //创建一条管道,pfd[0]为读端,pfd[1]为写端
pid = fork();
if(pid == 0)
{
char buffer[100];
while(1)
{
memset(buffer, 0, sizeof(buffer)); //清空buffer
read(pfd[0], buffer,sizeof(buffer)); //若没有内容则会阻塞
printf("子进程读取管道内容:buffer = %s\n", buffer);
}
exit(0);
}
char buffer[100] = {0};
int i = 0;
while(1)
{
scanf("%s", buffer);write(pfd[1], buffer, strlen(buffer)); //若缓冲区满了则会阻塞(缓冲区大小64kb)
}
return 0;
}
1.2)有名管道通信 fifo
特点:
有名字(不能在共享文件夹中创建);
任意进程中引用;
全双工通信,边读边写;
写入数据有原子性;
具备阻塞特性(无数据 / 管道缓冲区已满);
无法用 lseek 定位;
跟普通文件的操作方式一样
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(void)
{
if(access("myfifo", F_OK) != 0) //判断文件是否存在{
mkfifo("myfifo", 0664); //创建一个有名管道文件
}
int fd;
char buffer[100];
fd = open("myfifo", O_RDWR);
while(1)
{
memset(buffer , 0,
read(fd, buffer, sizeof(buffer));
printf("read buffer = %s\n", buffer);
}
close(fd);
return 0;
}
2)异步信号
适用于系统内部管理。
2.1)四个动作
single/sigaction
忽略 SIGKILL、SIGSTOP无法忽略
捕捉 收到这个信号让其做要求的动作
缺省动作 默认动作
sigprocmask
挂起
挂起优先级:大号实时信号 > 小号实时信号 > 非实时信号
输入命令查看62个信号:kill -l
ps. 杀戮信号 SIGKILL 和 停止信号 SIGSTOP 无法忽略、捕捉
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void sighand(int signum)
{
int i=10;
while(i--)
{
sleep(1);
printf("haha %d, i=%d\n", signum, i); //打印出接收到的信号编号和计数器的当前值
}
}
int main(void)
{
signal(SIGINT, sighand);
signal(SIGQUIT, sighand);
signal(SIGRTMIN, sighand);
signal(SIGINT, SIG_DFL); //将SIGINT信号的处理方式改回默认SIG_DFL,即终止程序
while(1); //无限循环,使得程序在没有接收到终止信号的情况下都不会退出
return 0;
}
2.2)非实时信号
1~31号信号
用途:系统控制进程
特点:
每一个非实时信号一般都对应着一个默认执行动作(缺省动作);
每一个非实时信号都有自己的名字;
每一个非实时信号都有自己触发的系统时间;
信号可以被嵌套执行;
信号丢失(用一个标记未记录)。
避免:
不要在异步操作中操作共有资源;
在主逻辑当中操作共有资源时应加阻塞
注意信号安全函数
2.3)实时信号
34~64号信号
特点:
每个信号都没有自己默认的执行动作;
每一个实时信号不一定有自己的名字;
信号可以被嵌套执行;
信号不会丢失(用一个计数变量来记录)。
ps.系统当中有两个信号就算设置了也回按照缺省动作执行动作:
SIGKILL:杀死进程
SIGSTOP:暂停进程
2.4)发送信号命令
用来发送一个指定信号给某个进程:
kill -s 发送的信号 进程的PID号
用来发送一个指定信号给某个应用程序(如果有多个同名的应用程序则都会收到):
killall -s 发送的信号 应用程序的名字
3)System-V IPC通信机制
3.1)概念
内核当中为了增强进程与进程之间数据交互及效率的一种机制的对象
3.2)组成
3.21)消息队列
增强型管道。具备管道的思想,可向里面发送及读取数据,每一个数据都可以夹带类型,还可以指定只读指定类型数据,其他数据继续存放在消息队列里
函数:msgsend、msgrcv
3.22)共享内存
在内核空间(物理内存)中开辟一块内存,映射给不同的进程的虚拟内存中,实现在不同进程中访问同一块内存,这是最快的一种进程间通信方式
函数:shmat、shmdt
3.23)信号量
操作某块资源的进程间的同步互斥体系,相当于一个增强型的全局变量(不同进程内),用来代表一种资源,没资源便进入睡眠
函数:semop
特点:
进程都可以访问这个变量;
进程可以加减这个变量;
当这个变量减到 0 时,继续减就会进入睡眠,只有等到变量的值继续减不小于 0,才会让进程继续工作。
3.24)posix有名信号量
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
#inlucde <time.h>
int main(void)
{
sem_t *poohsem;int retval;
poohsem = sem_open("poohsem", O_CREAT, 0664, 0);//0代表赋予的初值
struct timespec time;//定义时间结构体
clock_gettime(CLOCK_REALTIME, &time);//获取当前系统时间
time.tv_sec += 3;//超时+3秒
time.tv_nsec = 0;
while(1)
{
//sem_wait(mysem);
retval = sem_timedwait(mysem, &time);//P操作,带有超时检测
if(retval == -1)
{
perror("等待失败:");
return -1;
}
printf("P操作成功\n");
}
sem_close(mysem);
return 0;
}
3.3)操作流程
新建 IPC 对象,获取 IPC 的 key;
新建及初始化消息队列、共享内存、信号量对象;
根据不同的对象开始操作;
删除对象。
#include <stdio.h>
#include <stdlib.h>
#include < string.h>
#include <strings.h>
#include <unistd.h>
#include <fcnlt.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(void)
{
key_t key; //获取IPC的key
key = ftok(".", 1); //在当前路径创建key,值要大于0,例如这里是1
if(kdy == -1)
{
perror("获取IPC的key失败");
return -1;
}
int msg_id; //定义消息队列id
msg_id = msgget(key, IPC_CREAT|0664); //获取消息队列对象
if(msg_id == -1)
{
perror("获取消息队列失败");
return -1;
}
struct msgbuf buffer;
ssize_t rcv_size;
while(1)
{
msgrcv(msg_id, &buffer, sizeof(buffer), 250, 0);
printf("接收到%d消息%ld个字节的数据:%s\n", buffer.mtype, buffer.rcv_size, buffer.mtext);
}
return 0;
}
(9)进程的一生
三、线程
1、定义
调度的最小单位
2、现实应用场景
手机的微信可以同时跟不同聊天框交谈,微信就是一个资源,而在不同聊天框中交谈可以理解为在微信这一个资源里执行多任务。
3、特点
线程基于进程之上创建;
一个进程中允许多个线程;
线程共享统一进程资源;
CPU调度指令以线程为单位。
4、应用场景
需要程序同时做多件事情
5、线程属性
线程的分离属性(完全分离&可接合)一般默认创建的线程都是可接合属性
线程的栈空间大小
线程的优先级(实时&非实时)
6、线程间同步互斥
互斥锁
读写锁
条件变量
7、代码
(1)线程的创建、接合、退出
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void *new_thread(void *arg)
{
char *str = arg;
while(1)
{
sleep(1)
printf("new thread\n", str);
}
return "hi";
/*pthread_exit(“hi”);//退出线程*/
}
int main(void)
{
pthread_t tid;int retval;
retval = pthread_create(&tid, NULL, new_thread, "hello");
if(retval != 0) //或 if(retval < 0)
{
fprintf(stderr, "创建线程失败:%s\n", strerror(retval));
return -1;
}
void *retp;
/*pthread_join(tid, &retp); //接合函数
printf("线程结合成功:%s\n",(char *)retp);*/
pthread_join(tid, NULL); //接合函数
printf("线程接合成功\n");
/*pthread_exit(NULL);*/
return 0;
}
(2)线程的属性
2.1)分离属性
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void *new_thread(void *arg)
{
char *str = arg;
while(1)
{
sleep(1)
printf("new thread\n", str);
}
return "hi";
}
int main(void)
{
pthread_t tid;int retval;
pthread_attr_t attr; //创建属性变量
pthread_attr_init(&attr); //初始化属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置线程为完全分离属性
retval = pthread_create(&tid, NULL, new_thread, "hello");
if(retval != 0) //或 if(retval < 0)
{
fprintf(stderr, "创建线程失败:%s\n", strerror(retval));
return -1;
}
pthread_attr_destroy(&attr); //销毁属性
retval = ptherad_join(tid, NULL);
if(retval != 0)
{
fprintf(stderr, "接合线程失败:%s\n", strerror(retval));
return -1;
}
pthread_join(tid, NULL); //接合函数
printf("线程接合成功\n");
return 0;
}
2.2)栈的大小
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define _GNU_SOURCE
void *new_thread(void *arg)
{
char *str = arg;
pthread_t tid; //定义线程ID
pthread_attr_t attr; //定义线程属性
size_t stack_size; //定义栈大小
tid = pthread_self(); //获取本线程ID
pthread_getattr_np(tid, &attr); //获取线程属性
pthread_attr_getstacksize(&attr, &stack_size); //获取栈的大小
printf("栈的大小为:%ld\n", stack_size);
while(1)
{
sleep(1)
printf("new thread\n", str);
}
return "hi";
}
int main(void)
{
pthread_t tid;int retval;
pthread_attr_t attr; //创建属性变量
pthread_attr_init(&attr); //初始化属性
pthread_attr_setstacksize(&attr, 1024*1024*16); //设置栈的大小为16M
retval = pthread_create(&tid, NULL, new_thread, "hello");
if(retval != 0) //或 if(retval < 0)
{
fprintf(stderr, "创建线程失败:%s\n", strerror(retval));
return -1;
}
pthread_attr_destroy(&attr); //销毁属性
retval = ptherad_join(tid, NULL);
if(retval != 0)
{
fprintf(stderr, "接合线程失败:%s\n", strerror(retval));
return -1;
}
pthread_join(tid, NULL); //接合函数
printf("线程接合成功\n");
return 0;
}
2.3)优先级
2.31)实时线程(1~99级线程)
1~99静态优先级,静态优先级中数字越大,优先级越高
2.32)非实时线程(0级线程)
0静态优先级
2.33)同等优先级的调度策略
抢占式FIFO
轮询式RR
(3)线程的取消机制
pthread_cancel 发送取消请求 => state 取消状态(可被取消 / 不可被取消) type 取消类型(立即响应 / 遇到取消点相应)=>将不可被取消设置为可被取消时,就会响应过程中的取消请求
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void *new_thread(void *arg)
{
char *str = arg;
/*pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); //让线程无法被取消
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); //让线程不遇到取消点即刻退出
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); //让线程遇到取消点延迟退出*/
while(1)
{
sleep(1)
printf("new thread\n", str);
}
return NULL;
}
int main(void)
{
pthread_t tid;int retval;
retval = pthread_create(&tid, NULL, new_thread, "hello");
if(retval != 0) //或 if(retval < 0)
{
fprintf(stderr, "创建线程失败:%s\n", strerror(retval));
return -1;
}
sleep(2);
pthread_cancel(tid); //发送取消请求
pthread_exit(NULL);
return 0;
}
四、进程与线程的区别
左下图为拥有a、b两个程序的进程,每一个程序拥有独属于自己的一块资源。设定一个场景:a程序为聊天软件,当我们用手机打开它的时候就进入了一个进程,如果有人给我们发了一个网页视频,我们通过聊天框打开它进入那个网页,这个网页是另外一个程序b,这时就是打开了另外一个进程,b程序的开启不会让a程序被终止,而是在后台继续运行,就算再观看视频,有消息还是能正常接收,这种资源相互独立,互不干扰的就是进程。
右下图为一个线程,只拥有一块资源,在这块资源内部实现多任务操作。同样设定一个场景:该程序为聊天软件,当我们跟家人聊天的时候,也可以收到朋友发来的红包,收到公众号的推送,他们都是在该聊天软件中运行的,共用该聊天软件的资源,只是所做的事可能不同。