4 用户态和内核态信号量-实验3:创建Linux内核线程并使用内核信号量实现同步
一.实验目的
·掌握内核态信号量的的使用方法。
·理解内核态和用户态信号量的差异。
·理解内核线程的创建方法以及内核态、用户态线程的差异
二.实验背景
·创建—通过如下宏创建内核线程
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
·内核线程如何停止运行
int kthread_stop(struct task_struct *k);
bool kthread_should_stop(void);
·Linux 内核信号量的使用需要包含头文件<linux/semaphore.h>。Linux内核信号量的定义如下:
/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
从定义可以看出,内核信号量使用的其实是自旋锁;当一个内核进程试图获取内核信号量锁保护的资源时,该进程就会被挂起;只有在资源被释放时,进程才能变为可运行状态,另外,只有可重入函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。结构体中count表示信号量的值,大于0表示资源空闲;等于0则表示资源正在被其他进程使用,但没有进程在等待该资源;小于0则表示资源不可用,而且至少有一个进程在等待该资源。wait_list则是用来存放等待队列的链表地址,等待该资源的所有被挂起的进程都会放在这个链表中.
内核信号量的初始化有两种方法: 一种是使用宏DEFINE SEMAPHORE(sem),该宏把名为sem的信号量的值初始化为1:另一种则是使用初始化函数,运行如下:
static inline void sema_init(static semaphore *sem, int val)
该函数把信号量sem的值初始化指定数值,该数值由第二个传人的参数val定义。在Linux内核信号量中,wait和signal操作对应的函数为down和up其原型如下:
extern void up(struct seaphore * sem);
exterm void down(struct seaphore *sem);
·内核线程创建和停止
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
该宏的功能是创建一个内核线程,并调用wake_ up_process 使其开始运行。该宏的第一个参数 thredfn指向线程函数;第二个参数data是传递给线程函数的数据指针,第三个
参数namefmt则是线程函数的可用于打印输出的名称。从宏定义可以看出。Kthread_run实际上是调用kthrend_create,然后唤醒并运行创建的线程,kthread_rum 返回的参数是创建线程的任务控制块指针。
内核线程一旦启动后就会一直运行,除非该线程主动调用do_exit 函数退出,或者其他的线程调用kthread_stop 函数结束其运行,kthread_stop 函数原型如下:
int kthread_stop(struct task_struct *k);
该函数发送一个结束标志给需要撤销的线程,其中参数k指向要撤销的线程。如果要撒销的线程函数正在处理一个非常重要的任务,它可能不会被中断;如果线程函数永远不返回并且不检查信号,它将永远都不会停止。因此一般情况下,kthread_stop需要与另个函数kthrend_should_stop 配合工作。在需要撤销的内核线程中加人kthread_should_stop来检测是否需要接收到停止信号,该函数原型如下:
bool kthread_should_stop(void);
Kthread_should_stop 函数返同should_stop 标志(需要结束)是否为真。当运行的线程检测该标志为真时,线程将退出。虽然内核线程完全可以在完成自己的工作后主动结束,不需等待should_stop标志,但是如果在线程退出后主函数调用kthrend_stop 再次撤销该线程,将会发生 不可预测的后果。因为可能错误地关闭后续运行的重要服务例程。
三.关键代码及分析
static struct task_struct *test_task1;
static struct task_struct *test_task2;
struct semaphore sem1;
struct semaphore sem2;
int num[2][5] = {
{0,2,4,6,8},
{1,3,5,7,9}
};
int thread_one(void *p);
int thread_two(void *p);
int thread_one(void *p) //线程函数1
{
int *num = (int *)p;
int i;
for(i = 0; i < 5; i++){
down(&sem1); //获取信号量1
printk("kthread %d: %d ",current->pid, num[i]);
up(&sem2); //释放信号量2
}
if(current->mm ==NULL)
printk("\nkthread %d MM STRUCT IS null, actitve->mm address is %p\n",current->pid,current->active_mm);
while(!kthread_should_stop()){ //与kthread_stop配合使用
printk("\nkthread %d has finished working, waiting for exit\n",current->pid);
set_current_state(TASK_UNINTERRUPTIBLE); //设置进程状态
schedule_timeout(5*HZ); //设置唤醒、重新调度时间,5*HZ约等于5秒
}
return 0;
}
int thread_two(void *p) //线程函数2
{
int *num = (int *)p;
int i;
for(i = 0; i < 5; i++){
down(&sem2); //获取信号量2
printk("kthread %d: %d ",current->pid, num[i]);
up(&sem1); //释放信号量1
}
if(current->mm ==NULL)
printk("\nkthread %d MM STRUCT IS null, actitve->mm address is %p\n",current->pid,current->active_mm);
while(!kthread_should_stop()){ //与kthread_stop配合使用
printk("\nkthread %d has finished working, waiting for exit\n",current->pid);
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(5*HZ);
}
return 0;
}
static int kernelsem_init(void)
{
printk("kernel_sem is installed\n");
sema_init(&sem1,1); //初始化信号量1, 使信号量1最初可被获取
sema_init(&sem2,0); //初始化信号量2,使信号量2只有被释放后才可被获取
//sema_init(&sem2,2);//观察信号量初值不同导致的线程运行顺序
test_task1 = kthread_run(thread_one, num[0], "test_task1");
test_task2 = kthread_run(thread_two, num[1], "test_task2");
// 如果不适用 kthread_run,也已使用 kthread_create与wake_up_process ,二者配合使用
/*
int err;
test_task1 = kthread_create(thread_one, num[0], "test_task1");
test_task2 = kthread_create(thread_two, num[1], "test_task2");
if(IS_ERR(test_task1)){
printk("Unable to start kernel thread.\n");
err = PTR_ERR(test_task1);
test_task1 = NULL;
return err;
}
if(IS_ERR(test_task2)){
printk("Unable to start kernel thread.\n");
err = PTR_ERR(test_task2);
test_task2 = NULL;
return err;
}
wake_up_process(test_task1); //与kthread_create配合使用
wake_up_process(test_task2);
*/
return 0;
}
static void kernelsem_exit(void)
{
kthread_stop(test_task1); //如果线程使用while(1)循环,需要使用该函数停止线程
kthread_stop(test_task2); //本程序与while(!kthread_should_stop()配合使用
printk("\nkernel_sem says goodbye\n");
}
四.实验结果与分析
·make
·sudo insmod kernel_sem.ko
·dmesg
·sudo rmmod kernel_sem
·dmesg
检查mm是否为NULL的分支都为真,所以输出了线程的active–>mm 的地址,这表明运行的线程都是内核线程,其task_strucut 中的mm 地址为NULL。
然后,两个线程不断时执行while( !kthrand_should_stop))进行循环检测,并输出等待退出信息.当执行rmmod命令删除内核模块后,使用dmesg查看会发现该信息输出停止了。表明在rmmod命令删除内核模块时,调用了kthread_stop 函数,两个线程对kthread_should_stop 的检测停止,内核线程退出。在没有调用rmmod命令删除内核模块之前,通过 dmesg命令查看会发现信息会直增加。
最后,内核线程在 while (ktbread_should_stop)中调用 set_current_state 将当前线程设置为TASK_UNINTERRUPTILE, 并调用schedule_timeout 指明5* Hz时间后运行线程。在Linux中5*HZ相当于5秒钟。
习题:
15-1
广义的同步:竞争资源的多个进程按着特定的顺序执行,目的是使并发执行的进程之间能有效的共享资源和相互合作,从而使程序的执行具有可再现性。包含(狭义的)同步和互斥两种情况。
狭义的同步:异步环境下的一组并发进程,因直接制约而互相发送消息而进行相互合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。
互斥:一组并发进程中的一个或多个程序段,因共享某一公有资源而导致它们必须以一个不允许交叉执行的单位执行。
15-2
狭义同步包括单向同步问题,双向同步问题,同步于互斥结合。
单向同步问题:两个进程之间构成了单向同步制约,消费者受到生产者的制约不能任意运行,只能在收到生产者生产产品的通知后才能运行。
双向同步问题:在上面的单向同步问题中,在很多情况下,另一个进程存在对该进程的反向制约,两者之间存在了双向同步关系。双向同步问题从 本质上看就是两个单向同步问题的叠加。
同步与互斥结合:多个进程有不同的类型,同一类型之间存在互斥,不同类型之间可能存在单向同步或双向同步问题。
15-3
15-4
15-5
Linux内核线程的创建需要使用头文件<linux/kthread.h>内核现成的执行可以用一个宏来完成,定义为:
define kthread_run(threadfin,data,namefmt,…)
({struct task_struct *__k = kthread_create(threadfin,data,namefmt,## VA_ARGC);
if(!IS_ERR(_k
wake_up_process(__k);
__k;
})
该宏的功能是创建一个内核线程,并调用wake_up_process使其开始运行。该宏的第一个参数threadfn指向线程函数;第二个参数data是传递给线程函数的数据指针;第三个参数namefmt则是线程函数的可用于打印输出的名称,kthread_run实际上是调用kthread_create,然后唤醒并运行创建的线程。例如创建内核线程test_task1,test_task1=kthread(thread_one,num[0],”test_task1”);
在task_struct中有两个与进程地址空间相关的字段,一个是mm,一个是active_mm,两者都是struct mm_struct类型的指针,二者就是Linux用来区分用户线程和内核线程的主要依据。在Linux中当一个任务的task_struct的mm字段为NULL时,该任务就是内核线程,对于用户线程或进程来说,mm不为空,active_mm一般都与mm指向同一个地址。
练习:
15-1
#include <stdlib.h>
#include <memory.h>
#include <pthread.h>
#include <errno.h>
#include <math.h>
//筷子作为mutex
pthread_mutex_t chopstick[6] ;
void *eat_think(void *arg)
{
char phi = *(char *)arg;
int left,right; //左右筷子的编号
switch (phi){
case 'A':
left = 5;
right = 1;
break;
case 'B':
left = 1;
right = 2;
break;
case 'C':
left = 2;
right = 3;
break;
case 'D':
left = 3;
right = 4;
break;
case 'E':
left = 4;
right = 5;
break;
}
int i;
for(;;){
usleep(3); //思考
pthread_mutex_lock(&chopstick[left]); //拿起左手的筷子
printf("Philosopher %c fetches chopstick %d\n", phi, left);
if (pthread_mutex_trylock(&chopstick[right]) == EBUSY){
//拿起右手的筷子
pthread_mutex_unlock(&chopstick[left]);
//如果右边筷子被拿走放下左手的筷子
continue;
}
printf("Philosopher %c fetches chopstick %d\n", phi, right);
printf("Philosopher %c is eating.\n",phi);
usleep(3); //吃饭
pthread_mutex_unlock(&chopstick[left]); //放下左手的筷子
printf("Philosopher %c release chopstick %d\n", phi, left);
pthread_mutex_unlock(&chopstick[right]); //放下左手的筷子
printf("Philosopher %c release chopstick %d\n", phi, right);
}
}
int main(){
pthread_t A,B,C,D,E; //5个哲学家
int i;
for (i = 0; i < 5; i++)
pthread_mutex_init(&chopstick[i],NULL);
pthread_create(&A,NULL, eat_think, "A");
pthread_create(&B,NULL, eat_think, "B");
pthread_create(&C,NULL, eat_think, "C");
pthread_create(&D,NULL, eat_think, "D");
pthread_create(&E,NULL, eat_think, "E");
pthread_join(A,NULL);
pthread_join(B,NULL);
pthread_join(C,NULL);
pthread_join(D,NULL);
pthread_join(E,NULL);
return 0;
}
15-2
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#define BUFFER_SIZE 5 // 缓冲区数量
#define PRO_NO 10 // PRODUCING NO
#define OVER ( - 1)
#define PSLEEP 10000 //
#define CSLEEP 10000 //
#define PPNO 15 //
#define CPNO 10 //
sem_t lock; /* 信号量lock用于对缓冲区的互斥操作 */
sem_t notempty; /* 缓冲区非空的信号量 */
sem_t notfull; /* 缓冲区未满的信号量 */
struct prodcons
{// 缓冲区相关数据结构
int buf[BUFFER_SIZE]; /* 实际数据存放的数组*/
int readpos, writepos; /* 读写指针*/
int pid[BUFFER_SIZE];
};
struct prodcons buffer;
/* 初始化缓冲区结构 */
void init(struct prodcons *b)
{
b->readpos = 0;
b->writepos = 0;
}
/* 测试:生产者线程将0 到 PRO_NO的整数送入缓冲区,消费者线
程从缓冲区中获取整数,两者都打印信息*/
void *producer(void *data)
{
int n;
for (n = 0; n <= PRO_NO; n++)
{
sem_wait(¬full);
sem_wait(&lock);
/* 写数据,并移动指针 */
if (n < PRO_NO)
{
buffer.buf[buffer.writepos] = n;
buffer.pid[buffer.writepos] = getpid();
printf("%d ---> pid = %d\n", n, getpid());
usleep(PSLEEP);
}
else
{
buffer.buf[buffer.writepos] = OVER;
printf("%d --->\n", OVER);
}
buffer.writepos++;
if (buffer.writepos >= BUFFER_SIZE)
buffer.writepos = 0;
/* 设置缓冲区非空的条件变量*/
sem_post(¬empty);
sem_post(&lock);
}
return NULL;
}
void *consumer(void *data)
{
int d;
while (1)
{
/* 等待缓冲区非空*/
sem_wait(¬empty);
sem_wait(&lock);
/* 读数据,移动读指针*/
d = buffer.buf[buffer.readpos];
//usleep(CSLEEP);
buffer.readpos++;
if (buffer.readpos >= BUFFER_SIZE)
buffer.readpos = 0;
printf("--->%d pid = %d\n", d, buffer.pid[buffer.readpos]);
if (d == OVER)
break;
/* 设置缓冲区未满的条件变量*/
sem_post(¬full);
sem_post(&lock);
}
return NULL;
}
int main(void)
{
pthread_t th_c, th_p;
void *retval;
int i;
init(&buffer);
sem_init(&lock,0,1);
sem_init(¬empty,0,0);
sem_init(¬full,0,BUFFER_SIZE);
/* 创建生产者和消费者线程*/
pthread_create(&th_c, NULL, producer, 0);
pthread_create(&th_p, NULL, consumer, 0);
/* 等待两个线程结束*/
pthread_join(th_c, &retval);
pthread_join(th_p, &retval);
sem_destroy(&lock);
sem_destroy(¬empty);
sem_destroy(¬full);
return 0;
}