一、 实验目的
了解Linux中进程和线程的概念;
了解多线程程序的基本原理;
了解pthread库;
掌握用system、exec函数族、fork函数创建进程;
掌握使用pthread库中的函数编写多线程程序。
二、 实验任务与要求
应用fork函数创建子进程;
应用fork函数创建子进程,在父子进程中执行不同的任务;
新线程的创建,以及和主线程之间的关系。
三、 实验工具和环境
PC机、Linux Ubuntu操作系统。
四、 实验内容与结果
程序设计。
(1) 在父子进程中分别编写循环显示数字1-10和显示数字101-110的程序;
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
for (int i = 0; i <= 10; i++)
{
printf("subprocess: %d\n", i);
}
}
else
{
for (int i = 101; i <= 110; i++)
{
printf("process: %d\n", i);
}
}
}
使用了fork()函数来创建一个新的进程。fork()函数是Unix-like操作系统中创建进程的主要方法,它会复制当前调用它的进程,并返回一个进程ID。复制出来的进程称为子进程,原来的进程称为父进程。
这段代码中,首先调用了fork()函数,然后根据返回值判断是父进程还是子进程。如果返回值是0,说明是子进程,它会打印出0到10的数字;如果返回值不是0,说明是父进程,它会打印出101到110的数字。由于父进程和子进程是并发执行的,所以打印的顺序可能不一定固定。
(2) 应用函数sleep的不同参数等,体现程序中父子进程的并发运行。
在父子进程中分别执行不同的任务,即在子进程中编写程序1+2+…+10的程序,将该程序写入到sum.c中,并编译并运行该程序;在父进程中执行网络连通情况的测试任务,要求子进程退出后父进程才退出。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid == 0)
{
int fd;
int len, s_r, s_w;
char *buf = "#include<stdio.h>\
\nint main(){\
\n int i,s=0;\
\n for (i=1;i<=10;i++)\
\n s = s + i;\
\n printf(\"s=%d\\n\", s);\
\n}";
fd = open("sum.c", O_CREAT | O_RDWR, 0666);
len = strlen(buf);
s_w = write(fd, buf, len);
char buf_r[500];
lseek(fd, 0, SEEK_SET);
s_r = read(fd, buf_r, s_w);
buf_r[s_r] = "\0";
printf("Read:\n %s \n", buf_r);
system("gcc sum.c -o sum");
close(fd);
execlp("./sum", "sum", NULL);
}
else if (pid > 0)
{
system("ping 127.0.0.1 -c 2");
sleep(5);
}
else // fork失败
{
perror("fork error");
exit(0);
}
}
使用了fork()函数来创建一个子进程。子进程中,首先用open()函数打开一个名为sum.c的文件,如果文件不存在,就创建一个。然后用write()函数向文件中写入一段C语言代码,这段代码的功能是计算1到10的和并打印出来。接着用read()函数读取文件中的内容,并打印出来。然后用system()函数调用gcc命令来编译sum.c文件,生成一个名为sum的可执行文件。最后用execlp()函数执行sum文件,并传入一个参数"sum"。父进程中,用system()函数调用ping命令来测试本地网络连接,然后等待5秒。
调试下列程序。程序中使用pthread线程库创建一个新线程,在父进程(也称为主线程)和新线程中分别显示进程id和线程id,并观察线程id数值。(代码test3.c)问题如下:
// test3.c
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
pthread_t ntid;
void printids(const char *s){ /*各线程共享的函数*/
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid = %u tid = %u (0x%x) \n",s,(unsigned int)pid,(unsigned int)tid,(unsigned int)tid);
}
void *thread_fun(void *arg){ /*新线程执行代码*/
printids(arg);
return NULL;
}
int main(void){
int err;
/*下列函数创建线程*/
err = pthread_create(&ntid,NULL,thread_fun,"我是新线程:");
if(err != 0){
fprintf(stderr,"创建线程失败:%s\n",strerror(err));
exit(1);
}
printids("我是父进程");
sleep(2); /*等待新线程运行结束*/
return 0;
}
(1) 进程在一个全局变量ntid中保存了新创建的线程的id,如果新创建的线程不调用pthread_self而是直接打印这个ntid,能不能达到同样的效果?为什么?
不一定能达到同样的效果,因为ntid是一个全局变量,可能被其他线程修改或覆盖。而pthread_self返回的是当前线程的id,不会受到其他线程的影响。所以,为了安全和准确地获取线程的id,最好使用pthread_self函数。
(2) 在本题中,如果没有sleep(2)函数,会出现什么结果?为什么?
如果没有sleep(2)函数,可能会出现只打印出父进程的id,而没有打印出新线程的id的情况。这是因为父进程在创建新线程后,没有等待新线程运行结束,就直接退出了。而当父进程退出时,它的所有子线程也会被终止。所以,为了让新线程有机会执行,需要在父进程中调用sleep函数,让父进程暂停一段时间。
调试下列程序。程序中只给出了线程产生、退出和等待的程序,(代码 test4.c)根据该程序完成提出的问题:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void *thread_fun1(void *arg){
printf("thread 1 returning\n");
return ((void *)1);
}
void *thread_fun2(void *arg){
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}
int main(void){
int err;
pthread_t tid1,tid2;
void *tret;
err = pthread_create(&tid1,NULL,thread_fun1,NULL);
if(err!=0)
fprintf(stderr,"can't create thread 1: %s\n",strerror(err));
err = pthread_create(&tid2,NULL,thread_fun2,NULL);
if(err!=0)
fprintf(stderr,"can't create thread 2: %s\n",strerror(err));
err = pthread_join(tid1,&tret);
if(err!=0)
fprintf(stderr,"can't join with thread 1: %s\n",strerror(err));
printf("thread 1 exit code %d\n",(int)tret);
err = pthread_join(tid2,&tret);
if(err!=0)
fprintf(stderr,"can't join with thread 2: %s\n",strerror(err));
printf("thread 2 exit code %d\n",(int)tret);
exit(0);
}
(1) 修改程序,在线程1、2中分别完成1到1000的加法,观察线程的执行情况与线程结束时的状态,并给出执行结果的解释;
void *thread_fun1(void *arg){
int sum = 0;
for(int i = 1; i <= 1000; i++){
sum += i;
}
printf("thread 1 returning\n");
return ((void *)&sum);
}
void *thread_fun2(void *arg){
int sum = 0;
for(int i = 1; i <= 1000; i++){
sum += i;
}
printf("thread 2 exiting\n");
pthread_exit((void *)&sum);
}
定义了两个线程函数
thread_fun1
和thread_fun2
,它们的功能都是计算1到1000的和,并把结果存储在一个int类型的变量sum中。不同的是,thread_fun1
用return语句返回sum的地址,而thread_fun2
用pthread_exit()函数退出线程并传递sum的地址。为了运行这两个线程函数,需要用pthread_create()函数创建两个线程,并把它们的线程ID存储在一个pthread_t类型的变量中。然后,需要用pthread_join()函数等待这两个线程结束,并获取它们的返回值。最后,需要打印出这两个线程的返回值,它们都是500500。
(2)改变线程2的程序为计算1-100的阶乘,观察线程1和线程2执行任务的时间,观察结果。
void *thread_fun2(void *arg)
{
int i, j, num;
unsigned long long fact;
printf("计算1到100的阶乘\n");
for (i = 1; i <= 100; i++)
{
num = i;
fact = 1;
for (j = 1; j <= num; j++)
{
fact *= j;
}
printf("%d! = %llu\n", num, fact);
}
printf("thread 2 exiting\n");
pthread_exit((void *)1);
}
定义了一个线程函数
thread_fun2
,它的功能是计算1到100的阶乘,并把结果打印出来。阶乘是一个数的所有正整数因数的乘积,例如5! = 5 x 4 x 3 x 2 x 1 = 120。这段代码用一个for循环遍历1到100的每个数,然后用另一个for循环计算它的阶乘,并把结果存储在一个unsigned long long
类型的变量fact中。这个类型可以表示最大为18446744073709551615的无符号整数。然后,这段代码用printf()函数输出每个数和它的阶乘。最后,这段代码用pthread_exit()
函数退出线程,并传递一个值为1的void*
类型的指针。
编写程序,利用多线程实现生产者-消费者问题,生产者和消费者分别在2个线程中,生产者生产的产品放在链表的表头上,消费者从表头取走产品。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define MAX 12
sem_t empty;
sem_t full;
pthread_mutex_t mutex;
int buffer[MAX];
int count = 0;
void *producer(void *arg)
{
int i;
for (i = 0; i < MAX; i++)
{
sem_wait(&empty);
pthread_mutex_lock(&mutex);
buffer[count++] = i;
printf("producer: %d\n", i);
pthread_mutex_unlock(&mutex);
sem_post(&full);
}
}
void *consumer(void *arg)
{
int i;
for (i = 0; i < MAX; i++)
{
sem_wait(&full);
pthread_mutex_lock(&mutex);
int item = buffer[--count];
printf("consumer: %d\n", item);
pthread_mutex_unlock(&mutex);
sem_post(&empty);
}
}
int main()
{
pthread_t tid1, tid2;
sem_init(&empty, 0, MAX);
sem_init(&full, 0, 0);
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid1, NULL, producer, NULL);
pthread_create(&tid2, NULL, consumer, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);
return 0;
}
生产者和消费者,它们共享一个固定大小的缓冲区作为队列。生产者生成数据并放入缓冲区,而消费者从缓冲区取出数据并消费它。这段代码使用了信号量和互斥锁来协调对缓冲区的访问,避免数据不一致或溢出。
五、实验总结