linux内核使用C写的,文本重点在于理解内核、进程、线程的本质区别
内核就是一套软件,是OS这套系统对于所有硬件的协调软件, 他存在的初始目的就是去协调硬件,以根据用户给与的可读(阅读理解)指令进行硬件操作
负责对于CPU资源的分配,管理等,对于想要在当前OS上运行的软件,内部给与一个称呼,进程
当前系统主要核心目的是管理OS上运行的所谓软件
OS出现的目的是为了让我们更方便的计算机资源
存储设备管理子系统,主要是对于内存的管理建立起更加方便的操作方案
例如,在读写层面,1次一个地址(字节)比较慢,建立页表机制,4K做一次单位,对于所有地址进行统一规划
对于文件建立索引及管理策略
TCP/UDP/IP等基层协议的解析实现者
网卡驱动的管理协调者
输入/输出设备管理
外置设备管理
进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程:操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位
由于进程调度对CPU开销大,所以产生了线程的概念,线程有用户级线程、内核级线程实现方案
用户空间运行线程库,任何应用程序都可以通过使用线程库被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它提供多线程应用程序的开发和运行的支撑环境,包含:用于创建和销毁线程的代码、在线程间传递数据和消息的代码、调度线程执行的代码以及保存和恢复线程上下文的代码。
线程的创建、消息传递、线程调度、保存/恢复上下文都由线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)
创建销毁快,支持大量的用户线程(创建用户线程比内核线程需要的空间要小的多),但是不能利用CPU多核,同时需要自己实现阻塞调度,否则会影响其他用户线程的执行
内核级线程通常使用几个进程表在内核中实现,每个任务都会对应一个进程表。在这种情况下,内核会在每个进程的时间片内调度每个线程
所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程
从用户空间 -> 内核空间 -> 用户空间的开销比较大,但是线程初始化的时间损耗可以忽略不计。这种实现的好处是由时钟决定线程切换时间,因此不太可能将时间片与任务中的其他线程占用时间绑定到一起。同样,I/O 阻塞也不是问题
结合用户空间和内核空间的优点,设计人员采用了一种内核级线程的方式,然后将用户级线程与某些或者全部内核线程多路复用起来
在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度
采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。
对应用户级线程1:1的调度方式
对应系统级线程n:1的调度方式
对应混合线程n:m的调度方式
优点
备注:上下文指的是,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容
创建线程的代价要小于创建进程
与进程切换相比,线程之间的切换需要操作系统做的工作很少,线程切换只需要交换上下文信息即可
线程占用的资源比进程少很多
可以充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,系统可以执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
缺点
性能损失(如果一个进程中的多个线程在频繁的进行切换,则程序的运行效率会降低,因为性能损失在了线程切换当中。进程的执行效率,随着线程数量的增多,性能呈现出正态分布的状况)
代码健壮性降低(一个线程的崩溃,会导致整个进程的崩溃)
缺乏访问控制(多个线程在访问同一个变量时可能会导致程序结果的二义性)
下面示例中:主线程每隔1秒打印一次,共计打印3次,两个子线程每隔两秒打印,循环打印
结果:这里C++主线程停止后子线程也不再打印,但是java是可以继续打印
java与C++线程模式对比
java:java优化了线程,是一个多线程程序,属于上面的n:1的调度方式
C++:C++不去优化走内核策略,属于上面的1:1的调度方式
#include
#include
#include
#include
struct ThreadNum {
int thread_num_;
};
//函数指针去指定要运行的代码位置
void *MyThreadStrat(void *arg) {
struct ThreadNum *tn = (struct ThreadNum *) arg;
while (1) {
printf("%d号子线程\n", tn->thread_num_);
sleep(2);
}
delete tn;
return NULL;
}
int main() {
//线程信息
pthread_t tid;
// 功能 线程
// 创建 pthread_create()
// 退出 pthread_exit()
// 等待 pthread_join()
// 取消 pthread_cancel()
// 获取ID pthread_self()
// 调度策略 SCHED_OTHER、SCHED_FIFO、SCHED_RR
// 通信机制 信号、信号量、互斥锁、读写锁、条件变量
for (int i = 0; i < 2; i++) {
struct ThreadNum *tn = new ThreadNum;
if (tn == NULL) {
exit(1);
}
tn->thread_num_ = i;
int ret = pthread_create(&tid, NULL, MyThreadStrat, (void *) tn);
if (ret != 0) {
perror("pthread_create");
return 0;
}
}
int a = 0;
while (1) {
a++;
printf("主线程计数:%d\n", a);
sleep(1);
if (a == 3) {
break;
}
}
void *ret;
//需要自己协调线程状态
//pthread_join(tid, &ret);
return 0;
}
//0号子线程
//主线程计数:1
//1号子线程
//主线程计数:2
//0号子线程
//1号子线程
//主线程计数:3
在线程创建的例子中,如果需要做到跟java一样,等子线程结束后再终止整个进程,需要用到线程等待pthread_join,具体示例看上面注释部分
pthread_join(tid, &ret);
下面示例中,执行线程终止方法pthread_exit或者pthread_cancel,就不再打印
#include
#include
#include
void *MyThreadStrat(void *arg) {
// 两种退出方法,退出则不会打印下方内容
// pthread_exit(NULL);
// pthread_cancel(pthread_self());
printf("子线程 :%s\n", (char *) arg);
return NULL;
}
int main() {
pthread_t tid;
for (int i = 0; i < 2; i++) {
int ret = pthread_create(&tid, NULL, MyThreadStrat, NULL);
//start 是JAVA加入的概念,这里pthread_create就加入队列了
//tid.start();
if (ret != 0) {
perror("pthread_create");
return 0;
}
}
while (1) {
printf("主线程\n");
sleep(1);
}
return 0;
}
//子线程 :(null)
//主线程
//子线程 :(null)
//主线程
//主线程
一个线程被设置为分离属性,则该线程在退出之后,不需要其他执行流回收该进程的资源,而是由操作系统统一回收
下面示例中,子线程在延迟5秒之后调用线程分离,后续不再打印
#include
#include
#include
//线程分离
void *MyThreadStrat(void *arg) {
(void) arg;
int i = 0;
while (i < 10) {
i++;
printf("子线程\n");
pthread_cancel(pthread_self());
sleep(1);
}
sleep(5);
int err_code = pthread_detach(pthread_self());
printf("线程分离:%d\n", err_code);
return NULL;
}
int main() {
pthread_t tid;
void *ref;
int ret = pthread_create(&tid, NULL, MyThreadStrat, NULL);
pthread_join(tid, &ref);
if (ret != 0) {
perror("子线程创建错误");
return 0;
}
while (1) {
printf("主线程\n");
sleep(2);
}
return 0;
}
模拟了一个黄牛抢票的系统,使用4个线程同时去抢10张票,我们的预期结果是,4个线程每人拿到的票都不相同,不会拿到同一张票。
可以看到抢票重复了
#include
#include
#define THREADNUM 4
int g_val = 10;
//无锁数据异常案例
void *buyTicket(void *arg) {
while (1) {
if (g_val > 0) {
printf("%p抢到%d号票\n", pthread_self(), g_val);
g_val--;
} else {
break;
}
}
return NULL;
}
int main() {
pthread_t tid[THREADNUM];
for (int i = 0; i < THREADNUM; i++) {
int ret = pthread_create(&tid[i], NULL, buyTicket, NULL);
if (ret < 0) {
perror("pthread_create");
return 0;
}
}
for (int i = 0; i < THREADNUM; i++) {
pthread_join(tid[i], NULL);
}
return 0;
}
//0x700003eb8000抢到10号票
//0x700003eb8000抢到9号票
//0x700003eb8000抢到8号票
//0x700003eb8000抢到7号票
//0x700003eb8000抢到6号票
//0x700003eb8000抢到5号票
//0x700003eb8000抢到4号票
//0x700003eb8000抢到3号票
//0x700003eb8000抢到2号票
//0x700003eb8000抢到1号票
//0x700003f3b000抢到10号票
//0x700004041000抢到10号票
//0x700003fbe000抢到10号票
C++里面提供了互斥锁,没有synchronized关键字
java的synchronized关键字是自己维护了一个Monitor对象
#include
#include
#define THREADNUM 4
int g_val = 10;
//互斥锁
pthread_mutex_t g_lock;
//锁
void *buyTicket(void *arg) {
while (1) {
// step3: 即将访问临界资源,加锁,锁=计数器!
pthread_mutex_lock(&g_lock);
if (g_val > 0) {
printf("%p抢到%d号票\n", pthread_self(), g_val);
g_val--;
} else {
// step5: 可能会导致退出 解锁
pthread_mutex_unlock(&g_lock);
break;
}
// step4: 可能会导致退出 还锁
// 如果不在此处进行解锁的操作,则本次循环结束后,还是会进行拿锁,但是锁在上方并没有还掉
pthread_mutex_unlock(&g_lock);
}
return NULL;
}
int main() {
// step1: 初始化互斥锁
pthread_mutex_init(&g_lock, NULL);
pthread_t tid[THREADNUM];
for (int i = 0; i < THREADNUM; i++) {
int ret = pthread_create(&tid[i], NULL, buyTicket, NULL);
if (ret < 0) {
perror("pthread_create");
return 0;
}
}
for (int i = 0; i < THREADNUM; i++) {
pthread_join(tid[i], NULL);
}
// step2: 销毁互斥锁
pthread_mutex_destroy(&g_lock);
return 0;
}
//0x700009fab000抢到10号票
//0x700009fab000抢到9号票
//0x700009fab000抢到8号票
//0x70000a0b1000抢到7号票
//0x70000a134000抢到6号票
//0x70000a134000抢到5号票
//0x70000a134000抢到4号票
//0x70000a134000抢到3号票
//0x70000a134000抢到2号票
//0x70000a134000抢到1号票
如果执行流加载完毕之后不进行解锁操作则会造成死锁现象
上述互斥锁的示例中,去掉pthread_mutex_unlock则造成死锁,结果是卡死在这里
互斥条件:一个执行流获取了互斥锁之后,其他执行流不能获取该锁
不可剥夺:A执行流拿着互斥锁,其他执行不能释放
循环等待:多个执行流各自拿着对方想要的锁,并且各个执行流还去请求对方的锁
请求与保持:拿着一把锁还去申请别的锁
破环死锁的必要条件:循环等待,请求与保持
加锁顺序一致
避免锁未释放的场景
资源一次性分配
实际上就是消费者生产者模式,概念:
线程同步:在保证数据安全的情况下(互斥锁),让多个执行流按照特定的顺序进行临界区资源的访问,称之为同步
条件变量:是一个PCB等待队列,在该队列中存放的是线程或者进程的PCB。如果有多个线程,则将这些线程放入该等待队列,这些队列按照队列的方式顺序的出入队
下面示例中通过线程同步与条件变量,实现了生产者与消费者模式
对于条件变量一共使用了4个接口
step1:初始化接口
step2:销毁接口
step3:等待接口
step4:唤醒接口
其中唤醒接口分为
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
#include
#include
#include
int g_val = 1;
pthread_mutex_t g_lock;
//条件变量,实际上是一个队列
pthread_cond_t g_cond;
void *Product(void *arg) {
while (true) {
pthread_mutex_lock(&g_lock);
while (g_val >= 1) {
// step3:等待接口
// 等待消费者消费完
pthread_cond_wait(&g_cond, &g_lock);
}
g_val++;
printf("Product done..:%d\n", g_val);
pthread_mutex_unlock(&g_lock);
// step4:唤醒接口
// 通知消费者进行消费
pthread_cond_signal(&g_cond);
}
}
void *Consume(void *arg) {
while (true) {
pthread_mutex_lock(&g_lock);
while (g_val <= 0) {
// step3:等待接口
pthread_cond_wait(&g_cond, &g_lock);
}
g_val--;
printf("Consume done..: %d\n", g_val);
pthread_mutex_unlock(&g_lock);
// step4:唤醒接口
pthread_cond_signal(&g_cond);
}
}
int main() {
pthread_t pt, ct;
pthread_mutex_init(&g_lock, NULL);
// step1:初始化接口
pthread_cond_init(&g_cond, NULL);
pthread_create(&pt, NULL, Product, NULL);
pthread_create(&ct, NULL, Consume, NULL);
// step2:销毁接口
sleep(1);
pthread_cond_destroy(&g_cond);
pthread_join(pt, NULL);
pthread_join(ct, NULL);
return 0;
}
//Product done..:1
//Consume done..: 0
//Product done..:1
//Consume done..: 0
//循环...