0%

MPI学习笔记2

集合通信

MPI_Send和MPI_Recv可以实现不同进程之间的通信,但如果要跟很多进程发消息,或者要接受很多进程发送的消息,这样就太过麻烦。为此,MPI提供了一系列称为集合通信的函数,它涉及通信子之间的所有进程的通信。

MPI_Reduce

MPI_Reduce用于实现高效的全局运算,如求和,求最大值等。原型如下:

1
2
int MPI_Reduce(void* input_data_p,void* output_data_p,int count,
MPI_Datatype datatype,MPI_Op operator,int dest_process,MPI_Comm comm);

对于每个进程,我们调用MPI_Reduce,然后对目的进程,output_data_p指向的值会与每个进程的input_data_p指向的值进行运算。如果count参数的值大于1,则运算会在数组上进行。
支持的运算有:
|运算符值|含义|
|-|-|
|MPI_MAX|求最大值|
|MPI_MIN|求最小值|
|MPI_SUM|求累加和|
|MPI_PROD|求累乘值|
|MPI_LAND|逻辑与|
|MPI_BAND|按位与|
|MPI_LOR|逻辑或|
|MPI_BOR|按位或|
|MPI_LOR|逻辑异或|
|MPI_BOR|按位异或|
|MPI_MAXLOC|求最大值和最大值所在的位置|
|MPI_MINLOC|求最小值和最小值所在的位置|

一个示例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<mpi.h>
#include<bits/stdc++.h>
#include<sys/time.h>
using namespace std;

int main(int argc,char** argv){
int comm_sz,my_rank;
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
int val=my_rank*my_rank,sum=0;
MPI_Reduce(&val,&sum,1,MPI_INT,MPI_SUM,0,MPI_COMM_WORLD);
if(my_rank==0){cout<<sum<<endl;}
MPI_Finalize();
}

有两点值得注意:
1.第一个参数和第二个参数的指针不能相同,否则会得到非法的结果。
2.对于不是目标进程的进程,第二个参数实际上是没有作用的,可以置为NULL。

MPI_Allreduce

有时候,我们不仅需要全局运算,还需要把结果放到每个进程里,这时候可以调用MPI_Allreduce函数:

1
2
int MPI_Allreduce(void* input_data_p,void* output_data_p,int count,
MPI_Datatype datatype,MPI_OP operator,MPI_Comm comm);

参数的意义基本与MPI_Reduce相同,不再赘述。
用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<mpi.h>
#include<bits/stdc++.h>
#include<sys/time.h>
using namespace std;

int main(int argc,char** argv){
int comm_sz,my_rank;
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
int val=my_rank*my_rank,sum=0;
MPI_Allreduce(&val,&sum,1,MPI_INT,MPI_SUM,MPI_COMM_WORLD);
cout<<sum<<endl;
MPI_Finalize();
}

MPI_Bcast

有时候,我们需要将一个进程里的数据发送到通信子中的所有进程,这是可以调用MPI_Bcast函数:

1
int MPI_Bcast(void* data_p,int count,MPI_Datatype datatype,int souce_proc,MPI_Comm comm);

其中,data_p在发送进程里为输入参数,在其他进程里为输出参数,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<mpi.h>
#include<bits/stdc++.h>
#include<sys/time.h>
using namespace std;

int main(int argc,char** argv){
int comm_sz,my_rank;
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);

if(my_rank==0){
char* s="hello world!\n";
MPI_Bcast(s,strlen(s)+1,MPI_CHAR,0,MPI_COMM_WORLD);
}
else{
char *s=(char*)malloc(100*sizeof(char));
MPI_Bcast(s,14,MPI_CHAR,0,MPI_COMM_WORLD);//注意,这里s还是空字符串,不能用strlen确定长度。
printf("In process %d,s is %s",my_rank,s);
free(s);
}
MPI_Finalize();
}

MPI_Scatter

假如在主进程里有一个长为100的数组,要把它分到10个进程里,其中0号进程分到前10个元素,1号进程分到第10个到第19个元素……这时可以调用MPI_Scatter函数来进行分发:

1
2
3
int MPI_Scatter(void* send_buf_p,int send_count,
MPI_Datatype send_type,void* recv_buf_p,int recv_count,
MPI_Datatype recv_type,int src_proc,MPI_Comm comm);

假如数组大小为n,通信子里共有comm_sz个进程,则MPI_Scatter会把数组分为comm_xz份,每份有$local_n=\frac{n}{comm_xz}$个。此时send_count和recv_count应被置为local_n,因为send_count表示的是发送到每个进程的数据量,recv_count表示每个进程接收到的数据量。

一个示例程序如下:

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
#include<mpi.h>
#include<bits/stdc++.h>
#include<sys/time.h>
using namespace std;

int main(int argc,char** argv){
int comm_sz,my_rank;
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);
int n=100;
int* b=(int*)malloc(n/comm_sz*sizeof(int));
if(my_rank==0){
int* a=(int*)malloc(n*sizeof(int));
for(int i=0;i<n;i++){
a[i]=i;
}
MPI_Scatter(a,n/comm_sz,MPI_INT,b,n/comm_sz,MPI_INT,0,MPI_COMM_WORLD);
}
else{
MPI_Scatter(NULL,n/comm_sz,MPI_INT,b,n/comm_sz,MPI_INT,0,MPI_COMM_WORLD);
}
for(int i=0;i<n/comm_sz;i++)printf("In process %d, b[%d]=%d\n",my_rank,i,b[i]);
MPI_Finalize();
}

注意,跟MPI_Reduce一样,MPI_Scatter的两个指针参数不能相同,而且非目的进程的send_buff_p可以是NULL。

MPI_Gather

有了把数据发出去的方法,自然会有把数据收集起来的方法,它是MPI_Gather:

1
2
int MPI_Gather(void* send_buf_p,int send_count,MPI_Datatype send_type,void* recv_buf_p,
int recv_count,MPI_Datatype recv_type,int dest_proc,MPI_Comm comm);

用法和注意事项 与MPI_Scatter差不多,不再赘述

MPI_Allgather

MPI_Allgather与 MPI_Gather的关系有点类似MPI_allreduce与MPI_Reduce的关系

1
2
int MPI_Gather(void* send_buf_p,int send_count,MPI_Datatype send_type,void* recv_buf_p,
int recv_count,MPI_Datatype recv_type,int dest_proc,MPI_Comm comm);

用于将数据串联起来,存储到每个进程的recv_buf_p参数中。

综合应用

用上述集合通信的方法实现矩阵向量乘法的并行化:

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
#include<mpi.h>
#include<bits/stdc++.h>
#include<sys/time.h>
using namespace std;

void my_rand(double* vec,int size){
for(int i=0;i<size;i++)vec[i]=(double)rand()/RAND_MAX;
}

void Mat_vec_mul2(double* local_A,double* x,double* local_y,int local_m,int n,int local_n,MPI_Comm comm){
int local_i,j,local_ok=1;
for(local_i=0;local_i<local_m;local_i++){
local_y[local_i]=0.0;
for(j=0;j<n;j++)local_y[local_i]+=local_A[local_i*n+j]*x[j];
}
}
int main(int argc,char** argv){
int comm_sz,my_rank;
MPI_Init(NULL,NULL);
MPI_Comm_rank(MPI_COMM_WORLD,&my_rank);
MPI_Comm_size(MPI_COMM_WORLD,&comm_sz);

int m,n,local_m,local_n;
if(argc!=3)abort();
sscanf(argv[1],"%d",&m);
sscanf(argv[2],"%d",&n);
local_m=m/comm_sz,local_n=n/comm_sz;
double* local_A,* local_x,* local_y;
local_A=(double*)malloc(local_m*n*sizeof(double));
local_x=(double*)malloc(local_n*sizeof(double));
local_y=(double*)malloc(local_m*sizeof(double));
double *x=(double*)malloc(n*sizeof(double));
if(my_rank==0){
struct timeval tv,tv2;
gettimeofday(&tv,NULL);

double* A=(double*)malloc(m*n*sizeof(double));
double* y=(double*)malloc(m*sizeof(double)),*y2=(double*)malloc(m*sizeof(double));
my_rand(A,m*n),my_rand(x,n);
MPI_Scatter(A,local_m*n,MPI_DOUBLE,local_A,local_m*n,MPI_DOUBLE,0,MPI_COMM_WORLD);
MPI_Bcast(x,n,MPI_DOUBLE,0,MPI_COMM_WORLD);
Mat_vec_mul2(local_A,x,local_y,local_m,n,local_n,MPI_COMM_WORLD);
MPI_Gather(local_y,local_m,MPI_DOUBLE,y,local_m,MPI_DOUBLE,0,MPI_COMM_WORLD);

gettimeofday(&tv2,NULL);
printf("time: %d\n",tv2.tv_sec*1000000 + tv2.tv_usec - tv.tv_sec*1000000 - tv.tv_usec);
// for(int i=0;i<m;i++)cout<<y[i]<<" "<<y2[i]<<endl;
free(A),free(y),free(y2);
}
else{
MPI_Scatter(NULL,local_m*n,MPI_DOUBLE,local_A,local_m*n,MPI_DOUBLE,0,MPI_COMM_WORLD);
MPI_Bcast(x,n,MPI_DOUBLE,0,MPI_COMM_WORLD);
Mat_vec_mul2(local_A,x,local_y,local_m,n,local_n,MPI_COMM_WORLD);
MPI_Gather(local_y,local_m,MPI_DOUBLE,NULL,local_m,MPI_DOUBLE,0,MPI_COMM_WORLD);
}
free(local_A);free(local_y);free(x);
}

虽然这个程序使用了多进程的方式进行了优化,但事实上运行时间远远大于串行的版本:

并行:
image.png

串行:
image.png

MPI派生数据类型

显然,进程之间通信是非常非常慢的。如果主进程要往其他进程广播一个整数,两个浮点数,有没有方法能用一次通信的方法解决呢?
MPI为了解决这个问题,提供了用户自定义的派生数据类型。
我们可以用MPI_Type_create_struct函数创建由不同基本数据类型的元素所组成的派生数据类型:

1
2
int MPI_Type_create_struct(int count,int array_of_clocklengths[],
MPI_Aint array_of_displacements[],MPI_Datatype array_of_types[],MPI_Datatype* new_type_p);

第一个参数表明我们创建的数据类型有多少个元素;第二个参数表示每个元素是单个的数据还是一个数组;第三个参数表示每个元素相对于第一个元素的内存地址偏移量;第四个参数表示每个元素的类型;第5个参数表示创建的类型。

比如我们要创建一种数据类型,包含一个int,两个double,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
int a;
double b,c;
MPI_Aint a_addr,b_addr,c_addr;
MPI_Get_address(&a,&a_addr);
MPI_Get_address(&b,&b_addr);
MPI_Get_address(&c,&c_addr);
MPI_Aint array_of_displacements[3]={0,b_addr-a_addr,c_addr-a_addr};
int array_of_blocklengths[3]={1,1,1};
MPI_Datatype array_of_types[3]={MPI_INT,MPI_DOUBLE,MPI_DOUBLE};
MPI_Datatype input_mpi_t;
MPI_Type_create_struct(3,array_of_blocklengths,array_of_displacements,array_of_types,&input_mpi_t);
MPI_Type_commit(&input_mpi_t);

MPI_Get_address()用于获得第一个指针参数的绝对地址,把这个地址赋值给第二个参数指向的地址空间。
再调用完MPI_Type_create_struct()函数之后,还需要调用MPI_Type_commit()函数向通信子提交这一数据类型。之后,就可以像使用普通数据类型一样使用input_mpi_t了:

1
2
3
4
5
6
7
8
  if(my_rank==0){
a=1,b=1.1,c=2.2;
MPI_Bcast(&a,1,input_mpi_t,0,MPI_COMM_WORLD);
}
else{
MPI_Bcast(&a,1,input_mpi_t,0,MPI_COMM_WORLD);
printf("In process %d: a=%d b=%f c=%f\n",my_rank,a,b,c);
}

Pthread学习笔记2

临界区问题

当多个线程同时修改同一个内存地址时,可能会出现奇奇怪怪的问题。我们根据下面的公式编写一个计算π的程序:image.png

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
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
int thread_count,n;
double sum;

void* Thread_sum(void* rank){
long my_rank=(long)rank;
double factor;
long long i;
long long my_n=n/thread_count;
long long my_first_i=my_n*my_rank;
long long my_last_i=my_first_i+my_n;

if(my_first_i%2==0){
factor=1.0;
}
else factor=-1.0;
for(i = my_first_i;i<my_last_i;i++,factor=-factor){
sum+=factor/(2*i+1);
}
return NULL;
}

int main(int argc,char** argv){
if(argc!=3)abort();
long thread;
pthread_t* thread_handles;
thread_count=strtol(argv[1],NULL,10);
printf("ans: %f\n",sum);
n=strtol(argv[2],NULL,10);

struct timeval tv,tv2;
gettimeofday(&tv,NULL);
thread_handles=malloc(thread_count*sizeof(pthread_t));
for(thread=0;thread<thread_count;thread++)pthread_create(&thread_handles[thread],NULL,Thread_sum,(void*)thread);
//printf("Hello from the main thread\n");
for(thread=0;thread<thread_count;thread++)pthread_join(thread_handles[thread],NULL);
// for(int i=0;i<m;i++)printf("%f\n",y[i]);
gettimeofday(&tv2,NULL);
printf("ans: %.8f\n",4*sum);
printf("%d\n",tv2.tv_sec*1000000 + tv2.tv_usec - tv.tv_sec*1000000 - tv.tv_usec);

return 0;
}

执行代码,当线程数大于1的时候会发现,程序输出的结果是不确定的,而且误差也较1个线程的情况大,甚至运行时间也是较长:
image.png

这种情况被称为临界区问题,当0号线程把sum取到寄存器并执行运算的时候,1号进程也可能同时把sum取到寄存器,然后0号进程把结果写回内存,随后1号进程也把结果写回内存。这样就导致0号进程的计算结果被覆盖掉了,我们称

1
sum+=factor/(2*i+1);

这样更新共享资源的语句为一个临界区。当多个线程尝试更新一个共享资源时,结果可能是无法预测的。更一般地,当多个线程都要访问共享变量或共享文件这样的共享资源时,如果有一个线程执行了更新操作,就可能产生错误,我们称之为竞争条件

临界区问题可以用忙等待、互斥量和信号量等方法来解决。

忙等待

一种解决方法是设置一个标记变量,用于指明临界区可以被哪个线程执行,该执行完之后修改这个标记变量,而不能执行临界区的线程必须一直处于忙等待状态。
我们可以将代码修改成这样:

1
2
3
while(flag!=my_rank);
sum+=factor/(2*i+1);
flag=(flag+1)%thread_count;

flag是我们说到的标记变量,当flag==当前线程号时,可以执行临界区,否则必须处于忙等待状态。执行完之后要修改flag。

运行结果如下:
image.png
可以看到,结果是正确了,但运行速度满了很多,甚至比单线程的情况还慢。

当然,由于临界区只能被串行执行,我们应该尽量减少临界区执行的次数,比如,我们可以把Thread_sum函数修改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void* Thread_sum(void* rank){
long my_rank=(long)rank;
double my_sum=0;
double factor;
long long i;
long long my_n=n/thread_count;
long long my_first_i=my_n*my_rank;
long long my_last_i=my_first_i+my_n;

if(my_first_i%2==0){
factor=1.0;
}
else factor=-1.0;
for(i = my_first_i;i<my_last_i;i++,factor=-factor){
// printf("%d %d\n",flag,my_rank);
my_sum+=factor/(2*i+1);
}
while(flag!=my_rank);
sum+=my_sum;
flag=(flag+1)%thread_count;
return NULL;
}

用一个局部变量来代替sum变量,这样循环的过程中就不会有临界区。
image.png
运行结果正确,速度上也有提升。

使用忙等待还会有一个问题:

考虑一个最多可以同时执行2个线程的计算机,我们使用忙等待的方法编写一个5个线程的pthreads程序。假设开始时,操作系统运行0号线程和1号线程,这是没有问题的,此时2,3,4三个线程被挂起。然后假设0号线程运行结束,操作系统将其挂起,然后调度3号线程。由于操作系统的线程调度是随机的,它完全不知道此时只有2号线程能进入临界区,所以3号线程只能一直处于忙等待状态,浪费了时间。

所以,当线程数量大于计算机能同时运行的线程数时,使用忙等待的方法可能会产生问题。

互斥量

由于处于忙等待的线程会持续地占用CPU资源,所以忙等待并不是一个特别理想的解决临界区问题的方案。我们可以使用互斥量和信号量。

互斥量是互斥锁的简称,是一个特殊类型的变量,它可以保证每次只有一个线程能进入临界区。

互斥量的变量类型是pthread_mutex_t,必须调用 pthread_mutex_init()函数进行初始化。

当线程执行到临界区前面时,应该调用pthread_mutex_lock()函数。如果此时临界区已经被加了锁(即已经有其他线程执行到了这里),则它会被卡在临界区外等待;否则它会进入临界区,并把临界区加锁。

当临界区被执行完,线程要用pthread_mutex_unlock函数把临界区解锁。此时,系统会从其他等待的线程选出一个进入临界区。如果有多个等待的线程,操作系统选择哪个线程是随机的的。

互斥量使用完之后要用pthread_mutex_destroy函数将其销毁。

互斥量的示例如下:

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
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
int thread_count,n;
double sum;
int flag;
pthread_mutex_t mutex;
void* Thread_sum(void* rank){
long my_rank=(long)rank;
double my_sum=0;
double factor;
long long i;
long long my_n=n/thread_count;
long long my_first_i=my_n*my_rank;
long long my_last_i=my_first_i+my_n;

if(my_first_i%2==0){
factor=1.0;
}
else factor=-1.0;
for(i = my_first_i;i<my_last_i;i++,factor=-factor){
// printf("%d %d\n",flag,my_rank);
my_sum+=factor/(2*i+1);
}
pthread_mutex_lock(&mutex);
sum+=my_sum;
pthread_mutex_unlock(&mutex);
return NULL;
}

int main(int argc,char** argv){
if(argc!=3)abort();
long thread;
pthread_t* thread_handles;
thread_count=strtol(argv[1],NULL,10);
n=strtol(argv[2],NULL,10);
pthread_mutex_init(&mutex,NULL);
struct timeval tv,tv2;
gettimeofday(&tv,NULL);
thread_handles=malloc(thread_count*sizeof(pthread_t));
for(thread=0;thread<thread_count;thread++)pthread_create(&thread_handles[thread],NULL,Thread_sum,(void*)thread);
//printf("Hello from the main thread\n");
for(thread=0;thread<thread_count;thread++)pthread_join(thread_handles[thread],NULL);
// for(int i=0;i<m;i++)printf("%f\n",y[i]);
free(thread_handles);
pthread_mutex_destroy(&mutex);
gettimeofday(&tv2,NULL);
printf("ans: %.8f\n",4*sum);
printf("%d\n",tv2.tv_sec*1000000 + tv2.tv_usec - tv.tv_sec*1000000 - tv.tv_usec);

return 0;
}

运行结果:

image.png

信号量

信号量(semaphore)可以认为是一种特殊类型的unsigned int,它的类型是sem_t。信号量可以赋值为0,1,……。大多数情况下,我们只用0和1两个值。这种只有0和1值的信号量也称为二元信号量。0对应上了锁的互斥量,1对应没上锁的互斥量。

初始化信号量的方法是调用sem_init(),它的原型是:

1
int sem_init(sem_t* a,int b,int c);

其中第二个参数直接置为0就可以了,第三个参数是初始值。

如果我们要用信号量来解决临界区问题,可以先创建一个全局信号量,初始值为1。在临界区前调用sem_wait()函数,这个函数的意义是:如果信号量为0则阻塞;如果是非0值则减1然后进入临界区。临界区执行完之后,调用sem_post()函数将信号量置为1。

代码如下:

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
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>//注意要加这个头文件
#include<sys/time.h>
int thread_count,n;
double sum;
int flag;
sem_t my_semaphore;
void* Thread_sum(void* rank){
long my_rank=(long)rank;
double my_sum=0;
double factor;
long long i;
long long my_n=n/thread_count;
long long my_first_i=my_n*my_rank;
long long my_last_i=my_first_i+my_n;

if(my_first_i%2==0){
factor=1.0;
}
else factor=-1.0;
for(i = my_first_i;i<my_last_i;i++,factor=-factor){
my_sum+=factor/(2*i+1);
}
sem_wait(&my_semaphore);
sum+=my_sum;
printf("Thread %d\n",my_rank);
sem_post(&my_semaphore);
return NULL;
}

int main(int argc,char** argv){
if(argc!=3)abort();
long thread;
pthread_t* thread_handles;
thread_count=strtol(argv[1],NULL,10);
n=strtol(argv[2],NULL,10);
sem_init(&my_semaphore,0,1);

struct timeval tv,tv2;
gettimeofday(&tv,NULL);
thread_handles=malloc(thread_count*sizeof(pthread_t));
for(thread=0;thread<thread_count;thread++)pthread_create(&thread_handles[thread],NULL,Thread_sum,(void*)thread);
for(thread=0;thread<thread_count;thread++)pthread_join(thread_handles[thread],NULL);
free(thread_handles);
gettimeofday(&tv2,NULL);
printf("ans: %.8f\n",4*sum);
printf("%d\n",tv2.tv_sec*1000000 + tv2.tv_usec - tv.tv_sec*1000000 - tv.tv_usec);
return 0;
}

使用信号量还可以控制线程执行的顺序,如发送消息问题。我们可以给每个线程分配一个信号量,初始化为0,给每个线程一个char*,指向收到的消息。每一个线程把消息放到下一个线程后将下一个线程的信号量置为1,然后等待上一个线程的消息。代码如下:

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
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<semaphore.h>
#include<sys/time.h>
#define MSG_MAX 100
int thread_count,n;
double sum;
int flag;
pthread_mutex_t mutex;
char** messages;
sem_t* semaphores;
void* Thread_sum(void* rank){
long my_rank=(long)rank;
long dest=(my_rank+1)%thread_count;
char* my_msg=malloc(MSG_MAX*sizeof(char));
sprintf(my_msg,"Hello to %ld from %ld",dest,my_rank);
messages[dest]=my_msg;
sem_post(&semaphores[dest]);
sem_wait(&semaphores[my_rank]);
printf("Thread %ld > %s\n",my_rank,messages[my_rank]);
}

int main(int argc,char** argv){
if(argc!=2)abort();
long thread;
pthread_t* thread_handles;
thread_count=strtol(argv[1],NULL,10);
semaphores=(sem_t*)malloc(thread_count*sizeof(sem_t));
messages=(char**)malloc(thread_count*sizeof(char*));
struct timeval tv,tv2;
gettimeofday(&tv,NULL);
thread_handles=malloc(thread_count*sizeof(pthread_t));
for(thread=0;thread<thread_count;thread++)pthread_create(&thread_handles[thread],NULL,Thread_sum,(void*)thread);
//printf("Hello from the main thread\n");
for(thread=0;thread<thread_count;thread++)pthread_join(thread_handles[thread],NULL);
// for(int i=0;i<m;i++)printf("%f\n",y[i]);
free(thread_handles);
pthread_mutex_destroy(&mutex);
gettimeofday(&tv2,NULL);
printf("ans: %.8f\n",4*sum);
printf("%d\n",tv2.tv_sec*1000000 + tv2.tv_usec - tv.tv_sec*1000000 - tv.tv_usec);

return 0;
}

结果如下所示:
image.png

但如果我们把sem_wait那一行去掉,会出现这样的变化:
image.png
第0号线程没有收到消息的时候就调用printf函数,自然就得不到想要的结果。

像上面这种一个线程需要等待另一个线程执行某种操作的同步方式,有时称为生产者-消费者同步模型

Tensor与Autograd详解

参考自《深度学习框架PyTorch入门与实践》

Tensor

创建Tensor

创建Tensor的方法有很多:

函数 功能
Tensor(*size) 基础构造函数
ones(*size) 全为 1 Tensor
zeros(*size) 全为 0 Tensor
eyes(*size) 对角线为1,其他为0
arange(s,e,step) 从s到e,步长为step
linspace(s,e,step) 从s到e,均分为step份
rand/randn(*sizes) 均匀/标准分布
normal(mean,std)/uniform(from,to) 正态分布/均匀分布
randperm(m) 随机排列

用Tensor()函数创建tensor时,可以有很多方法:

1
2
3
4
5
6
7
8
9
x=t.Tensor([[1,2,3],[4,5,6]])#用一个list来创建
y=t.Tensor([1,2,3])
z=t.Tensor(1,2,3)#用几个值来确定Tensor的维度
print(x)
print(y)
print(y.tolist())
print(z)
a=x
a

image.png

1
2
3
4
5
6
print(x.size())
print(x.numel())
y=t.Tensor(x.size())
z=t.Tensor((2,3))#Tensor的元素是2和3
a=t.Tensor(2,3)#Tensor的大小是2*3
y,z,a

image.png

tensor.shape和tensor.size()是一样的:
image.png

一些其他的创建方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x=t.ones(2,3)
print(x)
x=t.zeros(4,5)
print(x)
x=t.arange(1,7,2)#注意,第二个参数是取不到的
print(x)
x=t.arange(1,7.00000000000001,2)#注意,第二个参数是取不到的
print(x)
x=t.linspace(1,10.7,5)
print(x)
x=t.randn(3,3)#均值为0,方差为1的正态分布
print(x)
x=t.randperm(20)
print(x)
x=t.eye(3,5)
print(x)

image.png

改变形状的操作

用 view(),squeeze(),unsqueeze(),resize_()等方法可以改变tensor的形状

view()

view()用于直接改变形状,但不能改变tensor的大小。用法:

1
2
3
b=t.arange(0,6)
print(b.view(2,3))
print(b.view(-1,2,1,3)) # -1表示自动计算其大小

image.png

unsqueeze()

unsqueeze()用于增加tensor的维度。用法:

1
2
3
4
print(b)
print(b.unsqueeze(1))#从 6 变成 6*1
print(b.unsqueeze(-2))#-2 的意思是,增加的那个维度是增加了维度之后的tensor的倒数第二个维度。
#也就是说,从 6 增加成 1*6

image.png

squeeze()

squeeze()用于压缩大小为一的维度。用法:

1
2
3
4
x=t.Tensor(2,3,1)
print(x.squeeze(0))#第0个维度大小是2,不能压缩
print(x.squeeze())#压缩所有大小为1的维度
x.squeeze(2)#第二个维度大小是1,被压缩

image.png

resize_

resize_也可以用来修改形状。不同的是,如果新尺寸超过了原来的尺寸,会自动分配新的内存空间。用法:

1
2
3
4
x=t.rand(2,3)
print(x)
print(x.resize_(3,2))
x.resize_(3,3)

image.png

索引操作

Tensor可以按下标索引:

1
2
3
4
5
6
7
8
9
a=t.randn(3,4)
print(a)
print(a[0])#第0行
print(a[:,2])#第2列
print(a[1:3,2:4])#第1,2行和第2,3列的交叉
print(a[:2])#前两行
print(a[0:2])#前两行
print(a[0:1,:2])#第0行,前两列。形状是1*2
print(a[0,:2])#第0行,前两列。形状是2

image.png

1
2
3
4
print(a)
print(a>1)
print(a[a>1])#形状不同
print(a*(a>1))#形状相同

image.png

选择函数

选择函数用于从Tensor中选出一部分。

index_select()

index_select()用于选出tensor特定维度特定下表的部分。用法:

1
2
3
4
5
6
7
8
import torch
a = torch.linspace(1, 12, steps=12).view(3, 4)
print(a)
b = torch.index_select(a, 0, torch.tensor([0, 2]))#0表示第0个维度,0,2表示这一维度下表为0和2的会被选出来。
print(b)
print(a.index_select(0, torch.tensor([0, 2])))
c = a.index_select(1, torch.tensor([1, 3]))#1表示第一个维度
print(c)

image.png

mask_select()

a.mask_select(b)=a[b],没啥好用的

nonzero()

用于获得非0元素的下标,用法:

1
2
3
a=t.Tensor([2,3,0,0,1])
print(a.nonzero())
a[a.nonzero()]

gather()和scatter()

太复杂了,等用到的时候再搞。

高级索引

x[[],[]……[]] 其中第1个列表表示第0个维度要选的东西,第2个列表表示第1个维度要选的东西,以此类推。

1
2
3
4
x=t.arange(0,27).view(3,3,3)
print(x)
print(x[[1,2],[1,2],[2,1]])#相当于x[1,1,2]和x[2,2,1]
print(x[[1,2],[0],[0]])#相当于x[1,0,0]和x[2,0,0]

image.png

Tensor类型

tensor的元素默认类型是FloatTensor,可以通过t.set_default_tensor_type()修改默认类型。

常见类型有:

数据类型 CPU tensor GPU tensor
32bit浮点 torch.FloatTensor torch.cuda.FloatTensor
64bit浮点 torch.DoubleTensor torch.cuda.DoubleTensor
16bit浮点 16bit浮点只能在GPU里用 torch.cuda.HaltTensor
8bit无符号整型 torch.ByteTensor torch.cuda.ByteTensor
8bit有符号整型 torch.CharTensor torch.cuda.CharTensor
16bit有符号整型 torch.ShortTensor torch.cuda.ShortTensor
32bit有符号整型 torch.IntTensor torch.cuda.IntTensor
64bit有符号整型 torch.LongTensor torch.cuda.LongTensor

还可以用type()函数指定类型:

1
2
3
4
5
6
7
t.set_default_tensor_type("torch.FloatTensor")
a=t.rand(2,3)
print(a)
b=a.int()
print(b)
b=b.type("torch.DoubleTensor")
print(b)

逐元素操作

这些函数会对tensor的每一个元素进行操作,如下表所示:

函数 功能
abs/sqrt/div/exp/fmod/log/pow

pytorch 快速入门

参考自《深度学习框架PyTorch入门与实践》

Tensor

Tensor是pytorch里最基础的数据结构,可以认为是一个高级数组。它可以包含一个数,一个一维数组,一个二维矩阵或更高维的数组。

Tensor的创建

创建一个Tensor的方法:

1
2
3
import torch as t
x=t.Tensor(5,3)
x

这种情况下,会生成一个内容全为0的Tensor:
image.png

随机生成Tensor的方法:

1
2
x=t.rand(2,3)
x

这样,Tensor里的每一个值都是一个0到1之间的随机数:
image.png

用t.ones()能生成全为1的Tensor,用t.zeros()能生成全为0的Tensor:

1
2
3
4
x=t.ones(2,3)
y=t.zeros(5,2)
print(x)
print(y)

image.png

得到Tensor的大小:

1
2
print(x.size())
x.size(0),x.size(1)

image.png

Tensor运算

Tensor之间的运算可以用 ‘+’、’-‘、’*’、’/‘等符号实现,也可以用add,sub等函数实现。

1
2
3
4
5
6
7
x=t.rand(2,3)
y=t.rand(2,3)
print(x)
print(y)
print(x+y)
print(x.add(y))
print(x)

image.png

值得注意的是,add函数并不会修改x的值,要实现x+=y,要用add_()函数:

1
2
3
4
5
6
7
x=t.rand(2,3)
y=t.rand(2,3)
print(x)
print(y)
print(x+y)
print(x.add_(y))
print(x)

image.png

Tensor与numpy的关系

Tensor的很多操作跟numpy差不多,它们也可以互相转换:

1
2
3
4
5
6
import numpy as np
x=np.ones(3)
y=t.from_numpy(x)
print(y)
z=y.numpy()
print(z)

image.png

值得注意的是,Tensor与nump对象的内存是共享的,因此其中一个改变的时候,另一个也会改变:
image.png

将Tensor转移到cuda上

可以通过.cuda方法把Tensor转移到GPU上:
image.png

Autograd

pytorch的Autograd模块提供了反向传播的功能。

autograd.Variable是Autograd中的核心类,它可以理解为Tensor的升级版。它的backward()方法能够自动计算梯度,提供了反向传播的功能。
image.png
image.png

由于y对x的每一个元素的偏导数都是1,所以x.grad是一个2*2的全为1的矩阵。
如果我们在调用一次y.backward(),会怎么样?
image.png

我们发现grad在反向传播的过程中是累加的,所以我们要将grad清零:
image.png

神经网络

torch.nn是专门为神经网络设计的模块化接口,可以用来定义和运行神经网络。nn.Module是nn中最重要的类,可以看成是神经网络的封装。

定义网络

一个网络需要继承nn.Module,并实现它的forward方法,并把具有可学习参数的层放在构造函数init中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.conv1=nn.Conv2d(1,6,5)#第一个参数是输入通道数,
#第二个参数是输出通道数,第三个参数是卷积核的大小。
self.conv2=nn.Conv2d(6,16,5)
self.fc1=nn.Linear(16*5*5,120)
self.fc2=nn.Linear(120,84)
self.fc3=nn.Linear(84,10)

def forward(self,x):
x=F.max_pool2d(F.relu(self.conv1(x)),(2,2))
x=F.max_pool2d(F.relu(self.conv1(x)),2)# 这里的2跟(2,2)的效果是一样的
x=x.view(x.size()[0],-1)
x=F.relu(self.fc1(x))
x=F.relu(self.fc2(x))
x=self.fc3(x)
return x

net=Net()
print(net)

image.png

只要实现了forward方法,backward方法就会被自动实现。
网络的可学习参数可以通过net.parameters()得到:
image.png

image.png

forward函数的输入和输出都是Variable,因此在输入的时候要把Tensor变成Variable:
image.png

注意,input是一个4维的向量,其中第一维是batch的大小,神经网络的输入是一个batch,里面可以有很多张图像。第二维是图像的通道数。第三维第四维是图像的大小。

损失函数

nn里定义了很多损失函数,如MSELoss用来计算均方误差,CrossEntropyLoss用来计算交叉熵损失。
image.png

优化器

torch.optim 中实现了很多优化方法,可以用它们来对网络的参数进行更新:

1
2
3
4
5
6
7
import torch.optim as optim
optimizer=optim.SGD(net.parameters(),lr=0.01)
optimizer.zero_grad()
output=net(input)
loss=criterion(output,target)
loss.backward()
optimizer.step()

CIFAR-10 分类

CIFAR-10是一个常用的彩色图片数据集,共有10个类别,每张图片的大小都是33232。
我们利用torchvision提供的CIFAR-10数据集来训练一个神经网络,并让它对测试图片进行预测。

首先进行一些准备工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch as t
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
show=ToPILImage()
#show用于将Tensor对象转化为图片,以便于观察结果
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))])
#transform 用于对输入图像进行预处理
trainset=tv.datasets.CIFAR10(root='/home/cy/data/',train=True,download=True,transform=transform)
trainloader=t.utils.data.DataLoader(trainset,batch_size=4,shuffle=True,num_workers=2)
#用trainloader加载训练数据,batch_size=4说明一个batch是4张图片,num_worker=2说明用两个线程
testset=tv.datasets.CIFAR10('/home/cy/data/',train=False,download=True,transform=transform)
testloader=t.utils.data.DataLoader(testset,batch_size=4,shuffle=False,num_workers=2)
classes=('plane','car','bird','cat','deer','dog','frog','horse','ship','truck')

查看某一张照片:

1
2
3
(data,label)=trainset[100]
print(classes[label])
show((data+1)/2).resize((100,100))

image.png

定义一个网络:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.conv1=nn.Conv2d(3,6,5)#第一个参数是输入通道数,
#第二个参数是输出通道数,第三个参数是卷积核的大小。
self.conv2=nn.Conv2d(6,16,5)
self.fc1=nn.Linear(16*5*5,120)
self.fc2=nn.Linear(120,84)
self.fc3=nn.Linear(84,10)

def forward(self,x):
x=F.max_pool2d(F.relu(self.conv1(x)),(2,2))
x=F.max_pool2d(F.relu(self.conv2(x)),2)# 这里的2跟(2,2)的效果是一样的
x=x.view(x.size()[0],-1)
x=F.relu(self.fc1(x))
x=F.relu(self.fc2(x))
x=self.fc3(x)
return x

net=Net()
print(net)

定义一个损失函数和优化器

1
2
3
from torch import optim
criterion = nn.CrossEntropyLoss()
optimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)

开始训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from torch.autograd import Variable
for epoch in range(2):
runnint_loss=0.0
for i,data in enumerate(trainloader,0):
inputs,labels=data
inputs,labels=Variable(inputs),Variable(labels)
#注意要将输入数据转化成Variable类型,不然无法训练
optimizer.zero_grad()
outputs=net(inputs)
loss=criterion(outputs,labels)
loss.backward()
optimizer.step()
runnint_loss+=loss.item()
if i%2000==1999:
print("[%d,%5d] loss: %3f"%(epoch+1,i+1,runnint_loss/2000))
runnint_loss=0.0
print("Finish trainning")

查看第一个batch的数据和标签:

1
2
3
4
dataiter=iter(testloader)
images,labels=dataiter.next()
print("实际的label:"," ".join("%08s"%classes[labels[j]] for j in range(4)))
show(tv.utils.make_grid(images/2-0.5)).resize((400,100))

image.png

查看它们的预测结果:

1
2
3
outputs=net(Variable(images))
_,predicted=t.max(outputs.data,1)
print("预测结果:","".join("%8s"%classes[predicted[j]] for j in range(4)))

查看总正确率:

1
2
3
4
5
6
7
8
9
10
11
correct=0
total=0
for data in testloader:
images,labels=data
outputs=net(Variable(images))
_,predicted=t.max(outputs.data,1)
#output.data是一个数组,max返回两个值,第一个是最大值,第二个是最大值所在的位置
total+=labels.size(0)
correct+=(predicted==labels).sum()
#根据最大值所在的位置是否跟labels相等可以判断分类是否正确
print("正确率为:%d %%"%(100*correct/total))

Pthreads学习笔记1

Pthreads用共享内存模型进行并行编程。一般地,程序会启动一个主线程,执行main函数中的代码,然后由主线程启动其他的线程。

Pthreads的hello world程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>

int thread_count;

void* Hello(void* rank){
long my_rank=(long)rank;
printf("Hello from thread %ld of %d\n",my_rank,thread_count);
return NULL;
}

int main(int argc,char** argv){
if(argc!=2)abort();
long thread;
pthread_t* thread_handles;
thread_count=strtol(argv[1],NULL,10);
thread_handles=malloc(thread_count*sizeof(pthread_t));
for(thread=0;thread<thread_count;thread++)pthread_create(&thread_handles[thread],NULL,Hello,(void*)thread);
printf("Hello from the main thread\n");
for(thread=0;thread<thread_count;thread++)pthread_join(thread_handles[thread],NULL);
free(thread_handles);
return 0;
}

这个程序里值得注意的是第16行至第22行。
第16行定义了一个pthread_t类型的指针。pthread_t用来存储线程的专有信息,可以作为线程的唯一标识,它的数据是由系统进行绑定的,用户无法访问。
在第17行,我们用命令行参数来得到线程数量。
18行给thread_handles分配了 thread_cound 个 pthread_t 对象的地址空间。
19行调用pthread_create()函数,它的原型为:

1
int pthread_create(pthread_t* thread_p,const pthread_attr_t* attr_p,void* (*start_routine)(void*),void* arg_p);

第一个参数是一个指向pthread_t对象的指针,且必须提前为它分配地址,以便pthread_create修改它指向的内容。第二个参数一般就是NULL,不用管。

第三个参数是一个指针,指向一个函数,这个函数只有一个参数,是一个指向void类型的指针,返回值也是一个指向void类型的指针。

第四个参数是一个指向void类型的指针,用它来给第三个参数指向的函数提供参数。

通过pthread_create()函数我们创建了一个线程,它要做的就是执行第三个参数指向的函数。在第19行里,我们给它的第三个参数是Hello函数,第四个参数是(void*)thread。

在这里,thread变量可以用来标志线程号,把它转化为(void*)指针是为了跟Hello函数的参数列表匹配。此后,在Hello函数里,我们又把它转化为long类型,这样我们就可以得到这个线程的线程号。

第21行调用了pthread_join()函数,顾名思义,它用来等待那个线程的结束。

第22行用来free掉thread_handles数组。

运行的结果如下:
image.png
注意,main函数执行第20行时,其他线程已经启动,所以输出的顺序是不确定的。

矩阵向量乘

用pthreads编写一个矩阵向量乘的程序:

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
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
int thread_count,n,m;
double** A,*x,*y;

void* Pth_mat_vect(void* rank){
long my_rank=(long)rank;
int i,j,local_m=m/thread_count;
int my_first_row=my_rank*local_m,my_last_row=my_first_row+local_m;
for(i=my_first_row;i<my_last_row;i++){
double tem=0.0,*A2=A[i];
for(j=0;j<n;j++){
tem+=A2[j]*x[j];
}
y[i]=tem;
}
return NULL;
}

int main(int argc,char** argv){
freopen("out2.txt","a",stdout);
if(argc!=4)abort();
long thread;
pthread_t* thread_handles;
thread_count=strtol(argv[1],NULL,10);
m=strtol(argv[2],NULL,10);
n=strtol(argv[3],NULL,10);

A=(double**)malloc(m*sizeof(double*));
for(int i=0;i<m;i++)A[i]=(double*)malloc(n*sizeof(double));
x=(double*)malloc(n*sizeof(double));
y=(double*)malloc(m*sizeof(double));
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
A[i][j]=(double)rand()/RAND_MAX;
}
}
for(int i=0;i<n;i++)x[i]=(double)rand()/RAND_MAX;

struct timeval tv,tv2;
gettimeofday(&tv,NULL);
thread_handles=malloc(thread_count*sizeof(pthread_t));
for(thread=0;thread<thread_count;thread++)pthread_create(&thread_handles[thread],NULL,Pth_mat_vect,(void*)thread);
//printf("Hello from the main thread\n");
for(thread=0;thread<thread_count;thread++)pthread_join(thread_handles[thread],NULL);
// for(int i=0;i<m;i++)printf("%f\n",y[i]);
gettimeofday(&tv2,NULL);
printf("%d\n",tv2.tv_sec*1000000 + tv2.tv_usec - tv.tv_sec*1000000 - tv.tv_usec);

free(thread_handles);
for(int i=0;i<m;i++)free(A[i]);
free(A),free(x),free(y);

return 0;
}

平均用时如下:(单位μs)
|矩阵大小\线程数|1|2|4|5|8|10|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|200|337 |282| 378 |895 |698| 969 |
|500|803 |562| 625 |543| 829 |956|
|1000|2716| 2499 |961 |1349| 1048| 1389|
|2000|15894 |5466 |2855 |3469 |2559| 2778|
|5000|65230 |30717 |26828 |20336 |13326| 14294|
|10000|294865| 133474| 65650 |77883 |50221| 53250|

加速比如下:

矩阵大小\线程数 1 2 4 5 8 10
200 1 1.19504 0.891534 0.376536 0.482808 0.347781
500 1 1.42883 1.2848 1.47882 0.968637 0.839958
1000 1 1.08683 2.82622 2.01334 2.5916 1.95536
2000 1 2.90779 5.56708 4.58172 6.21102 5.72138
5000 1 2.12358 2.43141 3.20761 4.89494 4.56345
10000 1 2.20916 4.49147 3.786 5.87135 5.53737

效率如下:

矩阵大小\线程数 1 2 4 5 8 10
200 1 0.59752 0.22288 0.075307 0.060351 0.034778
500 1 0.71441 0.3212 0.29576 0.12108 0.083996
1000 1 0.54342 0.70656 0.40267 0.32395 0.19554
2000 1 1.4539 1.3918 0.91634 0.77638 0.57214
5000 1 1.0618 0.60785 0.64152 0.61187 0.45635
10000 1 1.1046 1.1229 0.7572 0.73392 0.55374

在WSL里安装MPI非常方便,只需执行

1
sudo apt install -y build-essential mpich

即可。
这样就可以在终端中使用mpi了,但为了方便使用,还需在VSCode中添加路径。
在c_cpp_properties.json的includePath中添加”/usr/include/mpich”即可。

用mpi编写一个helloworld程序:

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<mpi.h>
using namespace std;
int main(int argc,char **argv){
int numprocs, myid;
MPI_Status status;
MPI_Init(&argc, &argv);//
MPI_Comm_rank(MPI_COMM_WORLD, &myid);//这里myid表示这个进程是第几个进程,如果myid是0则是第一个进程
MPI_Comm_size(MPI_COMM_WORLD, &numprocs); //numprocs表示总的进程数量
cout<<"helloworld\n";
MPI_Finalize();//
}

上述代码由许多值得注意的地方:

1.注意不能使用 bits/stdc++ 这个库,否则会出错。

2.main函数要带命令行参数。

3.MPI_Init函数表示从这一行开始,后面的代码会被并行执行,即在每个进程中执行一次。其他的MPI
函数必须在MPI_Init函数之后。

4.MPI_Comm_rank函数获得本进程是第几个进程,把进程号存储在myid中。第1个进程(也称为主进程)对应的myid是0,
第二个进程对应的myid是1……以此类推。

5.MPI_Comm_size得到进程的数量。

6.在网上查阅资料了解到,MPI_Finalize函数将并行部分终止,除了主进程外的进程都会被终止,主进程的串行部分还可以正常执行。但事实上我在本地运行程序时,MPI_Finalize函数之后的部分还是会被并行执行,我也不知道为什么。

写完之后,就可以在终端执行程序了。

image.png

用mpic++编译源代码,生成一个名为hw的可执行文件。再用mpirun运行文件,4表示4个进程,所以输出时4个helloworld。

利用MPI可以加速排序算法。
调用c++标准库的sort对1e7的数据进行排序,大约需要2.2秒的时间。

15826979841.jpg

使用MPI将程序并行化,可以大大加快速度。

方法一

将主进程待排序的数组分为两部分,送到两个子进程排序,排完之后再送到主进程,将它们合并起来。

代码如下:

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
#include<iostream>
#include<mpi.h>
#include<algorithm>
using namespace std;
const int MAX_size=1e7;
int main(int argc,char** argv){
int numprocs, myid, source;
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
int siz=MAX_size/2;
if(myid==0){
int *nums=new int[MAX_size+3];
for(int i=0;i<MAX_size;i++){
nums[i]=rand();
}
int t1=clock();
MPI_Send(nums,siz,MPI_INT,1,1,MPI_COMM_WORLD);
MPI_Send(nums+siz,siz,MPI_INT,2,2,MPI_COMM_WORLD);
int* rec1=new int[siz+3],*rec2=new int[siz+3];
MPI_Recv(rec1,siz,MPI_INT,1,1,MPI_COMM_WORLD,&status);
MPI_Recv(rec2,siz,MPI_INT,2,2,MPI_COMM_WORLD,&status);
int i=0,j=0,loc=0;
while(i<siz&&j<siz){
if(rec1[i]<rec2[j])nums[loc++]=rec1[i++];
else nums[loc++]=rec2[j++];
}
while(i<siz)nums[loc++]=rec1[i++];
while(j<siz)nums[loc++]=rec2[j++];
int t2=clock();
cout<<t2-t1<<endl;
}
else{
int* rec=new int[siz+3];
MPI_Recv(rec,siz,MPI_INT,0,myid,MPI_COMM_WORLD,&status);
sort(rec,rec+siz);
MPI_Send(rec,siz,MPI_INT,0,myid,MPI_COMM_WORLD);
}
MPI_Finalize();
}

大概需要1.2秒的时间:
1582699014283.png

方法二

将数组分为4部分,在4个子进程里排序,再两个两个合并起来,最后再送到主进程里合并。总共有7个进程。
代码如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include<iostream>
#include <mpi.h>
#include<algorithm>
using namespace std;
const int MAX_size=1e7;
int get_state(int myid){
if(myid==0)return 1;
else if(myid<3)return 0;
else return -1;
}
int main(int argc, char* argv[])
{
int numprocs, myid, source;
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
int state=get_state(myid);
if(state==1){
int* nums=new int[MAX_size+3];
int son1=(myid<<1)+1,son2=(myid<<1)+2;
for(int i=0;i<MAX_size;i++)nums[i]=rand();
int t1=clock();
MPI_Send(nums,MAX_size/2,MPI_INT,son1,myid*numprocs+son1,MPI_COMM_WORLD);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
MPI_Send(nums+MAX_size/2,MAX_size/2,MPI_INT,son2,myid*numprocs+son2,MPI_COMM_WORLD);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
int* rec1=new int[MAX_size/2+3],*rec2=new int[MAX_size/2+3];
MPI_Recv(rec1,MAX_size/2,MPI_INT,son1,myid*numprocs+son1,MPI_COMM_WORLD,&status);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
MPI_Recv(rec2,MAX_size/2,MPI_INT,son2,myid*numprocs+son2,MPI_COMM_WORLD,&status);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
int i=0,j=0,size=MAX_size/2,loc=0;
while(i<size&&j<size){
if(rec1[i]<rec2[j]){
nums[loc++]=rec1[i++];
}
else{
nums[loc++]=rec2[j++];
}
}
while(i<size){nums[loc++]=rec1[i++];}
while(j<size){nums[loc++]=rec2[j++];}
// for(int i=0;i<MAX_size;i++)cout<<nums[i]<<endl;
int t2=clock();
cout<<t2-t1<<endl;
}

else if(state==0){
int* nums=new int[MAX_size/2+3];
int* rec1=new int[MAX_size/4+3],*rec2=new int[MAX_size/4+3];
int son1=(myid<<1)+1,son2=(myid<<1)+2,fa=(myid-1)>>1;
MPI_Recv(nums,MAX_size/2,MPI_INT,fa,fa*numprocs+myid,MPI_COMM_WORLD,&status);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
MPI_Send(nums,MAX_size/4,MPI_INT,son1,myid*numprocs+son1,MPI_COMM_WORLD);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
MPI_Send(nums+MAX_size/4,MAX_size/4,MPI_INT,son2,myid*numprocs+son2,MPI_COMM_WORLD);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
MPI_Recv(rec1,MAX_size/4,MPI_INT,son1,myid*numprocs+son1,MPI_COMM_WORLD,&status);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
MPI_Recv(rec2,MAX_size/4,MPI_INT,son2,myid*numprocs+son2,MPI_COMM_WORLD,&status);
//cout<<myid<<" "<<son1<<" "<<son2<<endl;
int i=0,j=0,size=MAX_size/4,loc=0;
while(i<size&&j<size){
if(rec1[i]<rec2[j]){
nums[loc++]=rec1[i++];
}
else{
nums[loc++]=rec2[j++];
}
}
while(i<size){nums[loc++]=rec1[i++];}
while(j<size){nums[loc++]=rec2[j++];}
MPI_Send(nums,MAX_size/2,MPI_INT,fa,fa*numprocs+myid,MPI_COMM_WORLD);
}

else{
int* nums=new int[MAX_size/4+3];
int fa=(myid-1)>>1;
MPI_Recv(nums,MAX_size/4,MPI_INT,fa,fa*numprocs+myid,MPI_COMM_WORLD,&status);
// cout<<myid<<endl;
sort(nums,nums+MAX_size/4);
MPI_Send(nums,MAX_size/4,MPI_INT,fa,fa*numprocs+myid,MPI_COMM_WORLD);
}
// cout<<"??\n";
MPI_Finalize();
// cout<<myid<<"end\n";
} /* end main */

这种方法大约需要0.87秒:
1582699491051.png

方法三

将数组分到多个进程里排序,再送回主进程,直接进行排序:
代码如下:

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
#include<iostream>
#include<algorithm>
#include<mpi.h>
const int MAX_size=1e7;
using namespace std;
int main(int argc,char** argv){
int numprocs, myid, source;
MPI_Status status;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
int sub_size=MAX_size/(numprocs-1);
int siz[104]={0};
for(int i=1;i<numprocs;i++)siz[i]=MAX_size/(numprocs-1);
for(int i=1;i<=MAX_size%(numprocs-1);i++)siz[i]++;
if(!myid){
int* num=new int[MAX_size];
for(int i=0;i<MAX_size;i++)num[i]=rand();
int t1=clock();
int loc[numprocs]={0};
for(int i=1,*tem=num;i<numprocs;i++,tem+=siz[i]){
// cout<<i<<endl;
MPI_Send(tem,siz[i],MPI_INT,i,i,MPI_COMM_WORLD);
}
int** ans=new int*[numprocs];
// cout<<"??\n";
for(int i=1;i<numprocs;i++){
ans[i]=new int[siz[i]+3];
MPI_Recv(ans[i],siz[i],MPI_INT,i,i,MPI_COMM_WORLD,&status);
}
for(int i=0;i<MAX_size;i++){
int minval=(1ll<<31)-1,locc=0;
for(int i=1;i<numprocs;i++){
if(loc[i]<siz[i]&&ans[i][loc[i]]<minval)minval=ans[i][loc[i]],locc=i;
}
num[i]=minval,loc[locc]++;
}
int t2=clock();
cout<<t2-t1<<endl;
}
else{
int* num=new int[siz[myid]+3];
MPI_Recv(num,siz[myid],MPI_INT,0,myid,MPI_COMM_WORLD,&status);
sort(num,num+siz[myid]);
MPI_Send(num,siz[myid],MPI_INT,0,myid,MPI_COMM_WORLD);
}
MPI_Finalize();

}

对进程数量分别为4,5,6,7,8的情况做了5次实验,结果如下(单位为μs):

进程数 平均用时 最短用时 最长用时
4 996875 984375 1015625
5 868750 843750 890625
6 865625 843750 890625
7 890625 875000 906250
8 906250 859375 953125

1582702015001.png

fat12文件系统仿真实验报告

实验目的

  本实验目的在于学习fat12文件系统,增强编写C语言、C++语言和汇编语言程序的的能力,为进一步学习操作系统相关知识做好准备。

实验要求

总体要求

  用c、c++或汇编与c组合,编写一个具有下列功能的可视化或命令操作的FAT12文件系统仿真程序

具体要求

  用c、c++或汇编与c组合,编写一个具有下列功能的可视化或命令操作的程序:

  1.查看fdd144虚拟软盘的第一个扇区内容是否符合FAT12格式。
  2.检查fdd144虚拟软盘的FAT12格式各部分信息是否一致完整。
  3.列出fdd144虚拟软盘中根目录文件目录内容。
  4.列出fdd144虚拟软盘中目录树。
  5.提供一个输入框,输入任意字符,创建一个文件保存这些输入的字符。
  6.删除根目录的一个指定文件
  7.实现按路径名操作文件
  8.显示一个文件的内容
  9.编辑文件的内容
  10.复制文件,2个文件合并为一个文件。
  11.建立子目录
  12.删除子目录
  13.其他

实验方案与过程

实验环境

  首先通过WinImage软件创建一个虚拟软盘,往里面添加文件,保存后得到一个fat12格式的软盘。为了方便验证结果是否正确,我使用了HexView工具来查看软盘的内容。编写代码时,我主要在WSL(一个可以在windows操作系统下运行的linux简化版)系统下使用VSCode编辑器,用c语言和c++语言进行编程。

fat12文件系统相关知识

  一块1.44MB的fat12格式的软盘共有2880个扇区,每个扇区512占个字节。其中第0个扇区称为主引导扇区;第1-9扇区为fat表;第10-18扇区是fat表的复制版;第19-32扇区为根目录区;从第33扇区往后为数据区。

  字节和扇区在fat12文件系统中是两个相当重要的概念,为此,我们在程序中定义bt这个结构体来表示字节,sector这个结构体来表示扇区。为了方便行文,先给出它们的定义:

1
2
3
4
5
6
7
8
typedef struct byte//用bytebt来表示字节
{
char s[9];//把字节用01字符串表示
char h, l;//把字节用16进制表示,h为高4位,l为低4位
uc ascii;//这个字节的值,uc是unsigned char
void input();//从输入流中读取一个字节
void from_char(char c);
} bt;
1
2
3
4
5
6
7
8
typedef struct Sector
{
bt bs[SIZE_OF_SECTOR];//SIZE_OF_SECTOR默认是512
void input();//从输入流中读取扇区
void input_from_string(string a);//从一个长为512的字符串中读取扇区
void output();//打印扇区内容,方便调试
bt operator[](int i);//按下标取出一个字节
} sector;

创建软盘

  我们使用WinImage工具创建软盘。
  打开WinImage,点击左上角的文件、新建,然后确定,就创建了一块软盘,我们可以往里面添加文件,我随便拖了两个文件进去,结果如下
image.png
  按ctrl+s,将其保存为vfd文件。
  然后就可以通过HexView工具打开它。

引导扇区

  引导扇区主要记录了软盘的相关信息和引导扇区程序。启动计算机时,BIOS程序通过读入引导扇区中的引导扇区程序来启动操作系统。引导扇区的内容如下表所示:
(下表主要参考CSDN博客https://blog.csdn.net/begginghard/article/details/7284834?depth_1-utm_source=distribute.pc_relevant_right.none-task&utm_source=distribute.pc_relevant_right.none-task)
|名称|开始字节|长度|内容|参考值|
|–|–|–|–|–|
|BS_jmpBOOT|0|3|一个短跳转指令|0xEB 0x58 0x90|
|BS_OEMName|3|8|厂商名|’ZGH’|
|BPB_BytesPerSec|11|2|每扇区字节数(Bytes/Sector)|0x200|
|BPB_SecPerClus|13|1|每簇扇区数(Sector/Cluster)|0x1|
|BPB_ResvdSecCnt|14|2|Boot记录占用多少扇区|ox1|
|BPB_NumFATs|16|1|共有多少FAT表|0x2|
|BPB_RootEntCnt|17|2|根目录区文件最大数|0xE0|
|BPB_TotSec16|19|2|扇区总数|0xB40|
|BPB_Media|21|1|介质描述符|0xF0|
|BPB_FATSz16|22|2|每个FAT表所占扇区数|0x9|
|BPB_SecPerTrk|24|2|每磁道扇区数(Sector/track)|0x12|
|BPB_NumHeads|26|2|磁头数(面数)|0x2|
|BPB_HiddSec|28|4|隐藏扇区数|0|
|BPB_TotSec32|32|4|如果BPB_TotSec16=0,则由这里给出扇区数|0|
|BS_DrvNum|36|1|INT|13H的驱动器号|
|0|BS_Reserved1|37|1|保留,未使用|
|0|BS_BootSig|38|1|扩展引导标记(29h)|
|0x29|BS_VolID|39|4|卷序列号|
|0|BS_VolLab|43|11|卷标|
|’ZGH’|BS_FileSysType|54|8|文件系统类型|
|引导扇区程序|62|448|引导代码及其他数据|引导代码(剩余空间用0填充)|
|结束标志0xAA55|510|2|第510字节为0x55,第511字节为0xAA|0xAA55|

  BIOS程序通过识别结束标志判断该软盘是否符合fat12格式。

  在程序中,我们需要将软盘中的第一个扇区读入程序中,并判断它是否符合fat12格式。由于引导扇区本身就是一个扇区,所以定义sector的子类MBR,用以表示一个扇区。

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
struct MBR:public sector{
char BS_OEMName[8]; // OEM字符串,必须为8个字符,不足以空格填空
short BPB_BytsPerSec; // 每扇区字节数
char BPB_SecPerClus; // 每簇占用的扇区数
short BPB_RsvdSecCnt; // Boot占用的扇区数
char BPB_NumFATs; // FAT表的记录数
short BPB_RootEntCnt; // 最大根目录文件数
short BPB_TotSec16; // 扇区总数
char BPB_Media; // 媒体描述符
short BPB_FATSz16; // 每个FAT占用扇区数
short BPB_SecPerTrk; // 每个磁道扇区数
short BPB_NumHeads; // 磁头数
int BPB_HiddSec; // 隐藏扇区数
int BPB_TotSec32; // 如果BPB_TotSec16是0,则在这里记录
char BS_DrvNum; // 中断13的驱动器号
char BS_Reserved1; // 未使用
char BS_BootSig; // 扩展引导标志
int BS_VolID; // 卷序列号
char BS_VolLab[11]; // 卷标,必须是11个字符,不足以空格填充
char BS_FileSysType[8];// 文件系统类型,8个字符,不足填充空格

void init();//用于读入后初始化各个变量
bool check();//用于检查MBR是否合法
void print(int& Byte_Per_Sector,int& Sector_Per_Cluster,int& Num_Of_Fats,int& Root_Entry_Count,int& Num_Of_Sector,int& Size_Of_Fat,int& Sector_Hidden);//用于显示提示信息
};

  在程序开始时,我们首先读入MBR,调用init()函数对各个变量进行赋值;再检查它的前三个字节是否为0xeb 0x58 0x90;然后检查各个变量是否符合默认值;最后看结束标志是否为 0xaa55。如果发现问题,则给出提示信息并中止程序。如果检查正确,则调用print()函数输出各项信息。

fat区

  fat区共有18个扇区,其中后9个扇区是前9个扇区的副本。fat12文件系统启动时,需要检查这两部分是否一样。我们将前9个扇区称为fat表。从名字中就可以看出,fat表是fat12文件系统的核心。

  在fat12文件系统中,文件存储是以簇为单位的,一般情况下,一个簇就是一个扇区,所以下文将簇与扇区划等号。对于一个文件,如果它的大小小于512字节,则在数据区中给它分配一个簇,将它的内容写进这个簇里,然后将fat表中这个簇对应的值(下文会详述这种对应的方式)修改为一个特殊的结束标志。

  如果它的大小大于等于512字节呢?首先还是分配簇a,将文件的前512个字节写进簇a里,然后再分配一个簇b,将fat表中簇a对应的值修改为一个值,通过这个值我们能找到b扇区,然后再分配一个簇c,将fat表中簇b对应的值修改为一个值,通过这个值我们能找到簇c……一直这样下去,直至文件结束,将分配的最后一个簇在fat表中对应的值修改为结束标志。

  这样,读文件时,只需知道这个文件开始的簇,就可以读下一个簇,下下个簇……直到在fat表里读到结束标志为止。

  下图是一个fat表的开头:(这个fat表不是刚才创建的软盘的fat表)
image.png
  fat12文件系统中的”12”是什么意思呢?12是指一个fat表中12个bit组成一个fat表项,而我们知道一个字节是8个bit,所以fat表中,3个字节组成两个fat表项。

  既然如此,fat表该怎么读呢?如果你以为是顺着读,就太naive了。我们知道x86系统采用小端存储方式,就是说,低字节存储在高地址位中(高地址位就是地址值较小)。比如说,一个16位整数0000000011111111,在x86系统中会先存11111111(低字节),再存00000000(高字节)。
了解了这些,我们再来看fat表。前3个字节是 0xf0 0xff 0xff。我们把第二个字节的低四位拿出来,跟第一个字节拼在一起,组成0xff0(注意不是0xf0f),它就是fat表的第一个表项了。再把第三个字节拿出来,和第二个字节的高四位拼在一起,组成 0xfff,它就是第二个表项了,如下图所示:
![CEU3__8__V6@DE2`GG9IHO1.png](https://i.loli.net/2020/03/30/7RItOHvzNwFl6D5.png)
  按这个规则读下去,第三个表项就是0xfff,第四个表项是0x004……

  现在我们知道如何读这个fat表了。但问题来了,读出来的值有什么用呢?fat表中读出的第一个值表示坏簇标志,一般为0xff0,第二个值是结束标志,一般为0xfff。后续的每个值代表这个簇的下个簇的簇号。如第三个值为0x004,代表第一个簇(前两个值不算)对应的下一个簇是第二个簇,第四个值为0xfff,表示这个文件到第二个簇这里就结束了。

  在程序中,我们定义一个fat结构体:

1
2
3
4
5
6
7
8
9
10
11
typedef struct Fat
{
vector<bt> bts;//存储fat表的每一个字节
bt operator[](int i) { return bts[i]; }//按下标访问fat表中的字节
vector<int> cluster_numbers;//将fat表转换成一个整数数组,前三个字节没有包含在内
void init(MBR mbr);/*初始化,由于fat表的实际长度是大于数据区扇区数的,所以要将后面一部分切掉,这
个长度需要MBR中的一些变量来确定*/
int get_first_empty();//从fat表中读出一个空的簇号
void write_floopy();//将fat表写回软盘
int get_empty();//得到fat表中有多少个未被使用的簇和坏簇,用于检查是否有"孤魂野鬼"簇
} fat;

  程序开始时还是先把fat表读进来,并检查两个fat表是否一致,不一致就报错。然后统计空闲簇和坏簇的数量。

根目录区与数据区

  根目录区占14个扇区,由于前面已经用了19个扇区(引导扇区1个,fat18个),所以根目录区是从第19个扇区开始的,起始位置是0x2600。根目录里每32字节构成了一个文件记录,记录了这个文件的基本信息,我们可以根据这个记录来找到文件的内容。

  在程序中,我们定义rootentry类来表示根目录区,它比较简单,只有一个bt类型的数组,也不需要检查什么东西。

1
2
3
4
5
typedef struct RootEntry
{
vector<bt> bts;

} rootentry;

  从第33个扇区往后的扇区都是数据区,每个扇区512字节。定义datas类表示数据区。数据区一般包含2847个扇区,所以定义一个sector数组。

1
2
3
4
5
6
typedef struct Datas
{
vector<sector> secs;
void write_floopy(int clus_num, string content);/*这个函数用于写回软盘
在程序种写回软盘是以扇区为单位的。第一个参数表示写的是数据区第几个扇区,content是一个512字节的数组表示要写的内容。*/
} datas;

文件与文件夹

  文件的读写是fat12文件系统的重点。我们先来了解文件是怎么组织的。

  一个文件会有一个32个字节的文件记录,这个文件记录存储在这个文件所属的文件夹里,包含了这个文件的各种信息:

  第1-8个字节是文件名,如果长度不够就用空格填充;
  第9-11个字节是文件后缀名,如果长度不够或者没有后缀就用空格填充;
  第12个字节是文件属性,十分重要,一般的文件的文件属性就是0x00,文件夹的文件属性一般是0x10。
  第13-22个字节是保留字节,没有作用;
  第23,24字节是文件的创建时间
  第25,26字节是文件的创建日期,精确到分钟
  第27,28字节是文件的起始簇号
  第29-32字节是文件的大小
  我们以根目录为例:
image.png

  对于第一个文件,它的名字是NEW,后缀名是CPP,文件属性是0x00,说明它是一个普通文件。创建时间是0x89A4,换成二进制是

  1000 1001 1010 0100。

  其中前5位是小时,10001=17,说明创建时间是17点;后6位是分钟,001101=13,说明具体时间是17:13。后五位没有作用(因为5位最多表示32个数,而一分钟有60秒)

  创建日期是0x507E,换成二进制是

  0101 0000 0111 1110

  前7位是年份与1980的差值, 01010000=40,40+1980=2020,说明文件创建于2020年;后面4位是月份,0011是3,说明是2020年的3月;后面5位11110=30说明日期是 2020-3-30
起始簇号是0x0002

  大小是0x0000003c,说明它一共占60个字节。

  现在我们知道了这个文件的基本信息,我们要怎么找到它的内容呢?这就需要我们看一下数据区了。

  从第33个扇区往后的扇区都是数据区,每个扇区512字节。上文的NEW.CPP文件的起始簇号是2,由于fat表前两个值有其他用途,所以簇号2代表的是第一个簇,或者说数据区的第一个扇区。

  数据区的起始位置是0x4200,我们看看这里有什么:
image.png
  是60个a。

  再来看根目录下的下一个文件,起始簇号是3,代表它开始于数据区的第一个扇区,即0x4400

image.png

  它是一些奇奇怪怪的东西。

  根据前文所述的读文件方法,我们可以把整个文件读出来,在此不再赘述。

  打开前面创建的软盘,点击上方的映像、创建文件夹,然后输入名字,就可以创建一个文件夹了。
image.png
  往里面拖两个文件
image.png
  然后我们来看看根目录里发生了什么:
image.png
  可以看到它多了一个文件记录,名字是HOULAI,后缀是空的,文件属性是0x10,说明这是个文件夹,首簇号是0x2B,由于(0x2B-0x2)*0x200+0x4200=0x9400,我们只需要找到这个地址

image.png

  可以看到,这里也是一个个文件记录,其中有两个比较奇怪,第一个文件记录的名字是”.”,第二个文件记录的名字是”..”。事实上,除了根目录之外的每个文件夹都有这两个记录,”.”表示的是当前文件夹,”..”表示的是上一级文件夹。在程序中读取文件时它们会被忽略。

  了解了文件和文件夹的存储原理,在程序中读入文件的方法也就很简单了。

  在程序中,我们把文件和文件夹统一看成文件,用file类来表示它:

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
typedef struct File
{
string content, path;//分别是文件的内容和路径
string name, suffix;//分别是名字和后缀。文件夹没有后缀
int attr, fstclus, size;//分别是属性,首簇号,大小。文件夹的属性是0x10,普通文件的属性是0x00。
unsigned short Time, Date;//文件上一次修改的时间和日期
File *fa;//它的上一级文件夹的指针。
map<string, File *> sons;/*只有文件夹才有sons这个成员。
记录了它的子文件和子文件夹,用名字+"."+后缀来当索引*/
File() {}//默认构造函数
File(string Name, File *now);/*这个构造函数用于给now这个指针指向的文件夹
添加一个子文件夹,Name指定了文件夹的名字*/
File(string Name, string Content, File *now);/*这个构造函数用于给now这个指针指向的文件夹
添加一个子文件,Name指定了文件的名字和后缀*/
File(File *from);/*这个构造函数用于创建于from指向的文件内容相同的文件*/
unsigned short get_Time(int h, int m);//获得时间
unsigned short get_Date(int y, int m, int d);//获得日期
string ToString();//将时间和日期转换成string格式
string to_file_record();//根据文件的信息得到文件的文件记录
string to_dir_record();//根据文件夹的信息得到文件夹的文件记录
void add_file(File *tem_file);//给文件夹添加一个子文件
void add_file_root(File *tem_file);//跟根目录添加一个子文件
void get_content();//得到文件的内容
void write_back(int loc);//将修改后的文件内容写回软盘
File *get_son(string Name, bool isn_folder);//根据名字和后缀得到一个子文件(夹)
void dir();//输出子文件的信息
vector<string> dir_recusive();//递归得到所有子文件的信息
void remove();//删除这个文件
void get_content_deleted(int loc);/*对文件夹里已经被删除的目录项调用这个函数,
使得被删除的簇能被捕获,避免出现"孤魂野鬼"*/
void find_string(int loc, vector<string> &b, vector<string> &ret);/*从这个文件夹开始查找
将输入的字符串转化成b,将返回的路径字符串放在ret里*/
void create(File *tem);//用于创建于tem指向的文件内容相同的文件//
} file;

具体实现

  本系统完成了要求的所有功能,此外还增加了find功能,具体情况请看下表:

指令 功能 备注
help 输出提示信息
quit 退出系统
cd [相对路径或绝对路径] 进入指定的文件夹 “..”表示上一级文件夹,”.”表示当前文件夹,用’/‘作分隔符
dir 展示当前文件夹下的文件信息 会输出名字、后缀、修改时间和大小
dir_r 递归的展示当前文件夹下的所有文件 只能看到名字和后缀
print [相对路径或绝对路径] 输出文件内容 目标文件不能是文件夹
gedit [相对路径或绝对路径] 进入文本编辑器模式,编辑指定的文件
mk [文件名] [文件内容] 在当前路径下创建一个文件 文件名长度不能超过8,文件名不能包含非法字符,文件要有后缀,不能与已有文件同名
mkdir [文件夹名] 在当前路径下创建一个文件夹 文件夹名长度不能超过8,文件夹名不能包含非法字符,不能与已有文件夹重名
rm [相对路径或绝对路径] 删除一个文件或文件夹 删除操作不可逆,请谨慎操作
find [相对路径或绝对路径] 根据路径查找文件(夹) 支持通配符’‘和’?’。’‘可以表示任意长度的字符串,’?’可以表示任意字符。系统会输出所有可能的路径
cp [相对路径或绝对路径][文件夹的相对路径或绝对路径] 复制文件(夹) 将第一个参数复制到第二个参数指向的文件夹处
mv [相对路径或绝对路径][文件夹的相对路径或绝对路径] 剪切文件 将第一个参数剪切到第二个参数指向的文件夹处

  由于许多操作都需要按路径名找到文件,所有我们先来了解按路径名寻找文件的方法:

路径分析

  我们在file类中定义了一个map<string,file*>类型的成员变量sons,不用我说也能知道它的作用是什么。

  在创建一个文件时,如果它是一个文件夹,则对应它的所有子文件,往sons里加入一个(pair){文件名+”.”+后缀,指向该文件的指针}。这样,我们就能很轻松地根据文件名和后缀找到对应的文件。

  我们定义path_analysis和path_analysis2函数。path_analysis2函数接受一个字符串,将它用’/‘分隔为一个字符串向量并返回。

  用户输入路径可以是绝对路径或相对路径,当第一个字符为’/‘时,我们认为这是绝对路径,从根目录开始找;否则认为这是相对路径,从当前文件夹下开始找。

  从根目录开始找的方法是:

  设输入字符串向量为vec,根文件夹指针为f1。
  遍历向量的每一个元素,查找f1的sons里是否有子文件(夹)的名字与之匹配,若有则修改f1为对应文件(夹),否则直接返回NULL。
  遍历完向量之后说明f1已经是需要的文件(夹),返回f1即可。

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
file *path_analysis(string input, file *now, file *root)
{
if (input == "")
return NULL;
bool rt = (input[0] == '/');
for (auto i : input)
{
if (!isalpha(i) && !isdigit(i) && i != '/' && i != '.')
{
return NULL;
}
}
vector<string> folder_names = path_analysis2(input);
file *tem;
if (rt)
tem = root;
else
tem = now;
for (int i = 0; i < folder_names.size(); i++)
{
tem = tem->get_son(folder_names[i], i + 1 == folder_names.size());
if (tem == NULL)
{
return NULL;
}
}
return tem;
}

  从当前目录开始找的方法与此基本相同,不再赘述。

cd操作的实现

  既然了解了路径分析的方法,cd操作的实现也就呼之欲出了。我们在程序中定义两个指针,一个名为tem,指向当前所在的文件;一个名为root,指向根文件夹。只需要根据这两个指针和输入的字符串调用paht_analysis函数即可,如果函数返回NULL则报错,否则修改tem。

  代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if (a == "cd")
{
cin >> b;
file *now = path_analysis(b, tem, root);
if (now == NULL || now->attr != 0x10)
{
cout << "路径" << b << "不合法\n";
//注意,cd只能进入文件夹,进入一个文件是不可以的
}
else
{
tem = now;
}
}

dir操作的实现

  进行dir操作时,我们遍历sons,输出所有子文件(夹)的信息,包括文件名、后缀、修改时间和大小。由于这个操作十分简单,代码就不必展示了。

dir_r操作的实现

  这个操作的实现需要用到递归的思想。

  定义dir_recusive()函数,它返回一个字符串向量。它的实现方法是:
  创建一个vector ret用于返回。
  遍历sons的每一个键值对,如果值(即file指针)指向一个普通文件,则把对应的键放进ret里;
  如果指向的是一个文件夹,则对它调用dir_recusive(),把返回的字符串向量的每一个元素加上一个前缀” “,都放进ret里。
为什么要加入一个前缀呢?想一想,当前文件夹下的文件名没有前缀,下一级文件夹下的文件名有一个前缀,再下一级文件夹下的文件名有两个前缀……这样看起来不就形成了树的结构吗?

  dir_recusive()函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

vector<string> File::dir_recusive()
{
vector<string> vec, tem;
vec.push_back(name + (attr == 0x10 ? "" : "." + suffix) + "\n");
for (auto i : sons)
{
if (i.first == "." || i.first == "..")
continue;
tem = i.second->dir_recusive();
for (int j = 0; j < tem.size(); j++)
{
vec.push_back(" " + tem[j]);
}
}
return vec;
}

print操作的实现

  这个操作很简单,根据输入的路径找到相应的文件夹,然后输出它的内容即可。

mk操作的实现

  这个操作是重点和难点。首先我们要根据第一个参数找到文件夹,根据第二个参数创建一个文件。怎么创建一个文件?

  我们知道文件是保存在数据区里的,那我们要在数据区里找到一个空闲的扇区,这可以在fat表里找(也可以用已删除的文件占用的扇区,这会在下文讲解)一个空闲簇号,找到之后将内容写到对应的扇区,将fat表对应的位置修改为一个结束标志。如果文件内容不止512个字节,则继续分配一个簇号,将内容写进去,将fat表中上一个簇号对应的位置修改为一个这个簇号,将这个簇号对应的文职修改为一个结束标志。一直这样下去,直到写完为止。
  这部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
while (!Content.empty())
{
if (Content.size() > 512)
{
datas1.write_floopy(nowclus - 2, Content.substr(0, 512));
Content = Content.substr(512);
fat1.cluster_numbers[nowclus - 2] = fat1.get_first_empty() + 2;
nowclus = fat1.cluster_numbers[nowclus - 2];
fat1.cluster_numbers[nowclus - 2] = END_CLUSTER;
}
else
{
datas1.write_floopy(nowclus - 2, Content);
Content = "";
fat1.cluster_numbers[nowclus - 2] = END_CLUSTER;
}
}

  创建文件的其他操作就很简单了,注意要设置fa指针指向父文件夹。此外还要给文件生成一条文件记录,把它添加进父文件夹的内容里,给父文件夹的sons变量添加一个键值对。最后把fat表更新一遍。

mkdir操作的实现

  搞定了mk操作后,mkdir也就差不多了。区别是创建出来的文件的内容是两条文件记录,记录”.”和”..”这两个”子”文件夹。

rm操作的实现

  rm操作可能是最难的操作了。
  我们定义一个queue<pair<file , int>>类型的全局变量deleted_clusters。它的每一个元素是一个 file,int 对。

  当我们删除某个文件夹下的一个文件时,我们将它的文件记录的第一个字符变成一个删除标志,同时需要将 指向文件夹的指针 和 这个文件记录的序号 组成一个pair,push进deleted_clusters里。

  这样做有什么好处呢?假设我们在删除操作完成后就退出系统,然后重新打开,读入文件夹时,我们如果读到一个首字符为删除标志的文件记录,就把指向文件夹的指针和文件记录的序号放进deleted_clusters里。

  此后,如果我们需要再创建一个文件,就可以去deleted_clusters找被删除的簇。怎么找呢?我们先通过deleted_culsters的front()方法得到一个pair,根据它找到之前删除文件的文件记录,它不是记录了这个文件在数据区的位置了吗?直接将这些位置覆盖就可以了。覆盖完之后将这个pair pop掉就可以了。

  这样就大功告成了吗?还没有,至少还有两个问题:

  1.如果被删除的文件占用两个扇区,而我这次创建的文件只有一个扇区,那我把删除文件的首扇区覆盖了,第二个扇区不就成了孤魂野鬼了?

  2.将被删除文件的内容覆盖了之后,我们并没有修改它的文件记录,这样,我们下一次启动系统,还是会将它对应的pair读到deleted_clusters里,这样就可能又把它覆盖了一遍,不是就乱套了吗?

  其实解决方法也很简单。

  第一个问题,只需将首扇区覆盖之后,看一下这个扇区在fat表中对应的值是否是结束标志,如果不是,就修改文件记录,使它的首扇区变成原来的第二个扇区。同时,由于这个文件还没有被完全覆盖,因此不必在deleted_clusters里把它对应的pair pop掉。

  第二个问题,只需要将文件记录里的首扇区标志改为0,此后读文件时遇到这种首扇区标志位0的文件记录就跳过即可。

  以上就是删除普通文件的基本方法。删除文件夹呢?也很简单,递归地把子文件都删除,再将文件夹也删除了就可以了。

  为什么要这样?因为我的deleted_clusters是一个队列,由于子文件先删除,队列又是先进先出的,覆盖的时候便会先覆盖子文件,再覆盖文件夹。否则要是先把文件夹覆盖了,子文件的文件记录找不到了,就出现”孤魂野鬼”了。如果deleted_clusters是一个栈,就应该先删除文件夹,再删除文件。

  删除操作的代码如下:

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 File::remove()
{
if (attr == 0x10)
{
for (auto i : sons)
{
if (i.first == "." || i.first == "..")
continue;
i.second->remove();
}
}

for (int j = 0; j < fa->content.size(); j += 32)
{
if ((fa->content[j + 27] << 8) + fa->content[j + 26] == fstclus)
{
fa->content[j] = DELETED_CLUSTER_SIGN;
fa->sons.erase(attr == 16 ? name : name + "." + suffix);
fa->write_back(j / SIZE_OF_SECTOR);
deleted_clusters.push({fa, j / 32});
break;
}
}
}

gedit操作的实现

  为了实现这个操作,我设计了一个简易的编辑器。它的原理是不断相应键盘的输入,每输入一次就把控制台刷新一次,然后输出。由于这个编辑器与fat12文件系统关系不大,我就不再赘述。

  这个操作也是根据输入的字符串找到相应的文件(注意这个文件不能是文件夹),然后调用编辑器编辑它的内容,当用户按下ctrl+s则将修改后的内容保存回去。怎么保存呢?很简单,只需把原来的文件删掉,再创建一个同名而不同内容的文件即可。

find操作的实现

  find操作本事不算复杂,但为了使我的文件系统更有特色,我给它增加了’*’和’?’这两个通配符。

  ’‘表示任意字符串(甚至可以是空的)。比如 “houlai”和”h\lai”是匹配的。
  ’?’表示单个字符。比如”zhiqian”和”?hi?ian”是匹配的。

  怎么实现字符串的带通配符匹配呢?一种方法是暴力法,即直接枚举通配符的每一种可能。但这样效率很低,在字符串很长的情况下根本配不出来。

  另一种是动态规划法,即dp(dynamic planning),对输出长度为n和m的字符串,它的时间复杂度是O(nm)的,效率很高。由于这部分内容与fat12文件系统关系不大,具体的dp方法就不必赘述,直接给出匹配的代码:

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
bool isMatch(string a, string b){
int n = a.size(), m = b.size();
bool ok[n + 1][m + 1];
for (int i = 0; i <= n; i++)
for (int j = 0; j <= m; j++)
ok[i][j] = false;
bool okk[m + 1];
for (int j = 0; j <= m; j++)
okk[j] = false;
okk[0] = 1;
ok[0][0] = 1;
for (int j = 1; j <= m; j++)
{
ok[0][j] == (ok[0][j - 1] && (b[j - 1] == '*'));
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if (b[j - 1] == '?')
{
ok[i][j] = ok[i - 1][j - 1];
}
else if (b[j - 1] == '*')
{
ok[i][j] = okk[j - 1];
}
else
{
// cout << ok[i - 1][j - 1] << " " << (a[i - 1] == b[j - 1]) << endl;
ok[i][j] = (ok[i - 1][j - 1] && (a[i - 1] == b[j - 1]));
}
okk[j] = okk[j] || ok[i][j];
//cout << i << " " << j << " " << ok[i][j] << endl;
}
}
return ok[n][m];
}

  搞定了单个字符串的匹配方法,find操作的实现也就简单了,只需像之间路径分析一样搜索下去,找到了就将路径放进一个vector<string>里。不同的是这里的find可以找到多个路径,因此需要用dfs的方法;而之前的路径分析最多只有一个结果,因此直接找就可以了。
  具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void File::find_string(int loc, vector<string> &b, vector<string> &ret)
{
if (loc == b.size())
{
// cout<<name<<" "<<path<<endl;
ret.push_back(path);
return;
}
for (auto i : sons)
{
if (isMatch(i.first, b[loc]))
{
i.second->find_string(loc + 1, b, ret);
}
}
}

cp操作和mv操作

  在明确了如何找文件,如何删除文件之后,这两个操作也就非常简单了,没有进行解释的必要。

代码架构

  总共有12个文件,分别是:

文件名 内容
fun.cpp 定义了各种通用的函数
byte.h 定义了byte类
byte.cpp byte类的实现和与之相关的函数
sector.h 定义了sector类和它的实现
mbr.h 定义了MBR类
mbr.cpp MBR类的实现
file.h 定义了file类、fat类、rootentry类和datas类。
file.cpp 上述几个类的实现和相应的函数。
getch.cpp 定义了一个函数
my_editor.h 定义了my_editor类和一些函数
interface.cpp 给出了my_editor类的大部分实现,定义了一些与用户交互的函数
main.cpp 包括了程序开始时的检查和根文件夹的读入

  总计约1900行代码

结果展示

  使用dir_r可以展示目录树:
image.png
  使用print可以显示文件内容:
image.png
  使用gedit可以编辑文件:
image.png
image.png
  使用dir命令可以查看当前文件夹下的内容:
image.png
  使用cd命令可以进入文件夹:
image.png
  使用mk命令可以创建文件,使用rm文件可以删除文件:
image.png
  使用mkdir命令可以创建文件夹:
image.png
  使用cp命令可以复制文件:
image.png
  使用mv命令可以剪切文件:
image.png
  使用rm命令还可以删除文件夹:
image.png

  详情请看演示视频。

实验总结

  这次实验可以说是我做过最难的实验了,代码量大,编程难度高,还用到了搜索、动态规划等算法。但从最后的完成情况来看,我还是比较满意的(虽然还是有很多bug),实现了要求的功能,还增加了自己的创意。不过还是存在代码不够规范的问题,不同文件、不同类糅杂在一起,以至于我自己都不清楚我的代码文件是怎么组织的。

  在完成这次实验的过程中,最大的困难是编写删除操作的代码。我会思考了很久才想出一种很复杂的方法,这也带来了大量的代码和bug,不过好在还是写出来了。但在跟其他同学交流之后,似乎他们的方法更为简单,而我的方法理论上在性能方面会更优秀(我也不太确定),为了偷个懒我也没有改。

  还有一个遗憾是没有用到汇编语言。事实上大部分代码是用c++编写的,还用到了stl和linux的各种头文件里的很多内容,可能会导致代码的可移植性有点差。

  这次实验的收获也是很明显的,我锻炼了编写较大规模程序的能力,对文件系统和操作系统有了更深入的了解,也激发了我进一步学习操作系统的兴趣。

  总的来说,这次实验完成的十分艰难,但也是收获满满,有一些做的不错的地方,也有不少改进的空间。希望在接下来的课程中也能有这么有趣的实验。

CUDA 学习笔记

主要参考《GPU高性能编程CUDA实战》

一般将CPU及其系统的内存称为主机,将GPU及其内存称为设备。
在GPU设备上执行的函数通常称为核函数(Kernal);

一个简单的程序:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

__global__ void kernel(void){
}

int main(void){
kernel<<<1,1>>>();
printf("Hello world\n");
return 0;
}

global修饰符告诉编译器,函数应该被编译为在设备上而不是在主机上运行。于是,kernel()函数将被交给编译设备代码的编译器,而main()函数将被交给主机编译器。
尖括号表示要将一些参数传递给运行时系统,用以告诉运行时如何启动设备代码。

再来看一个更复杂的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <cuda_runtime.h>
__global__ void add( int a, int b, int *c ) {
*c = a + b;
}
int main( void ) {
int c;
int *dev_c;
//cudaMalloc()
cudaMalloc( (void**)&dev_c, sizeof(int) );
//核函数执行
add<<<1,1>>>( 2, 7, dev_c );
//cudaMemcpy()
cudaMemcpy( &c, dev_c, sizeof(int),cudaMemcpyDeviceToHost ) ;
printf( "2 + 7 = %d\n", c );
//cudaFree()
cudaFree( dev_c );

return 0;
}

cudaMalloc()用于分配内存,但与malloc()不同的是,它是在GPU上分配内存。由于分配的地址时在GPU上,所以程序员一定不能在主机代码中对这个指针进行解引用。同样的,也不能用free()函数来释放这些内存,只能用cudaFree()。
为了让主机代码能得到GPU计算的结果,可以用cudaMemcpy()函数,它的第四个参数数据移动的方向,是从主机到设备、设备到主机还是设备到设备。

上面说的这些内容好像对加快程序的执行速度没什么帮助,我们用下面这个向量求和的例子来了解CUDA如何让程序变得更快:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<book.h>
#define N 10
__global__ void add(int* a,int* b,int* c){
int tid=blockIdx.x;
if(tid<N)c[tid]=a[tid]+b[tid];
}

int main(void){
int a[N],b[N],c[N];
int* dev_a,*dev_b,*dev_c;
HANDLE_ERROR(cudaMalloc((void**)&dev_a,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_b,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_c,N*sizeof(int)));
for(int i=0;i<N;i++)a[i]=-i,b[i]=i*i;
HANDLE_ERROR(cudaMemcpy(dev_a,a,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_b,b,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_c,c,N*sizeof(int),cudaMemcpyHostToDevice));
add<<<N,1>>>(dev_a,dev_b,dev_c);
HANDLE_ERROR(cudaMemcpy(c,dev_c,N*sizeof(int),cudaMemcpyDeviceToHost));
for(int i=0;i<N;i++)printf("%d %d\n",i,c[i]);
cudaFree(dev_a),cudaFree(dev_b),cudaFree(dev_c);
return 0;
}

注意到,我们用了add<<<N,1>>>。N代表了设备在执行核函数时使用的并行线程块(Block)的数量。当启动核函数时,我们告诉运行时,我们想要一个一维线程格(Grid),其中包含N个线程块。
如何在代码中知道当前正在运行的是哪个线程块?可以通过blockIdx.x得到,它是一个内置变量。这样,我们就把向量求和并行化,达到了加速的效果。

在上面的例子中,我们启动了多个线程块,但每个线程块只有1个线程。要想在每个线程块里启动多个线程,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<book.h>
#define N 100
__global__ void add(int* a,int* b,int* c){
int tid=threadIdx.x+blockIdx.x*blockDim.x;
if(tid<N)c[tid]=a[tid]+b[tid];
}

int main(void){
int a[N],b[N],c[N];
int* dev_a,*dev_b,*dev_c;
HANDLE_ERROR(cudaMalloc((void**)&dev_a,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_b,N*sizeof(int)));
HANDLE_ERROR(cudaMalloc((void**)&dev_c,N*sizeof(int)));
for(int i=0;i<N;i++)a[i]=-i,b[i]=i*i;
HANDLE_ERROR(cudaMemcpy(dev_a,a,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_b,b,N*sizeof(int),cudaMemcpyHostToDevice));
HANDLE_ERROR(cudaMemcpy(dev_c,c,N*sizeof(int),cudaMemcpyHostToDevice));
add<<<(N+127)/128,128>>>(dev_a,dev_b,dev_c);
HANDLE_ERROR(cudaMemcpy(c,dev_c,N*sizeof(int),cudaMemcpyDeviceToHost));
for(int i=0;i<N;i++)printf("%d %d\n",i,c[i]);
cudaFree(dev_a),cudaFree(dev_b),cudaFree(dev_c);
return 0;
}

注意到启动核函数时用的是<<<(N+127)/128,128>>>,(N+127)/128是对N/128向上取整,得到线程块的数量。第二个128代表一个线程块有128个线程。这样子可能会导致线程数量大于N,但没有关系,我们会在核函数里判断线程索引是否越界。
在核函数中,线程索引通过$tid=threadIdx.x+blockIdx.x*blockDim.x$得到。
为了避免使用过多的线程块,我们直接指定线程块的数量,然后在核函数中用while循环对下表递增,如下所示:

1
2
3
4
__global__ void add(int* a,int* b,int* c){
int tid=threadIdx.x+blockIdx.x*blockDim.x;
while(tid<N)c[tid]=a[tid]+b[tid],tid+=blockDim.x*gridDim.x;
}

安卓开发笔记3

(随手写的,不具有参考价值)

给EditText自定义边框

(主要参考这篇博客:https://www.cnblogs.com/johnsonwei/p/5785055.html)

需要在drawable文件夹下面自定义一个xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="#EEEEEF"/>//内部颜色
<corners
android:radius="15dip"//用于指定边框4个角的弯曲程度
/>
<stroke
android:width="0.5px"
android:color="#225050"/>//边框颜色
</shape>
</item>
</layer-list>

然后指定EditText的background:

1
2
3
4
5
6
7
<EditText
android:layout_width="200dp"
android:layout_height="wrap_content"
android:inputType="phone"
android:text=""
android:id="@android:id/edit"
android:background="@drawable/edit_bg"/>