广州大学学生实验报告 ,实验一 进程控制与进程互斥

广州大学学生实验报告

开课学院及实验室: 计算机科学与网络工程学院  电子楼418B        2022年 9月 26日

学院

计算机科学与网络工程学院

年级/专业/班

计科 

姓名

Great Macro

学号

 

实验课程名称

Unix/Linux 操作系统分析实验

成绩

实验项目名称

进程控制与进程通信

指导  老师

张*玲

实验一 进程控制与进程互斥(1)

  • 实验目的

1、理解Linux下进程的结构;

2、理解Linux下产生新进程的方法(系统调用—fork函数);

3、掌握如何启动另一程序的执行;

4、理解Linux下线程的结构;

5、理解Linux下产生新线程的方法;

6、理解Linux系统下多进程与多线程的区别

  • 实验内容和任务

1、利用fork函数创建新进程,并根据fork函数的返回值,判断自己是处于父进程还是子进程中;

2、在新创建的子进程中,使用exec类的函数启动另一程序的执行;分析多进程时系统的运行状态和输出结果;

3、利用最常用的三个函数pthread_create,pthread_join和pthread_exit编写了一个最简单的多线程程序。理解多线程的运行和输出情况;

4、利用信号量机制控制多线程的运行顺序,并实现多线程中数据的共享;

5、分析Linux系统下多进程与多线程中的区别。

  • 实验涉及的系统调用函数

1、fork(  )   创建一个新进程。

    系统调用格式: pid=fork( )

    参数定义:int  fork(  )

    fork(  )返回值意义如下:

   0:在子进程中,pid变量保存的fork(  )返回值为0,表示当前进程是子进程。

   >0:在父进程中,pid变量保存的fork(  )返回值为子进程的id值(进程唯一标识符)。

   -1:创建失败。

2、exec类的函数: 启动另一程序的执行;

 

3、pthread_create函数:创建一个线程

它的原型为:

extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,void *(*__start_routine) (void *), void *__arg));

第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。

4、函数pthread_join:等待一个线程的结束。函数原型为:

extern int pthread_join __P ((pthread_t __th, void **__thread_return));

第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。

这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。

5、函数sem_init()用来初始化一个信号量。它的原型为:

extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value));

sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。

6、函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。

函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。

8、函数sem_destroy(sem_t *sem)用来释放信号量sem。

  • 实验步骤

1、利用fork函数编写一个简单的多进程程序,用ps命令查看系统中进程的运行状况,并分析输出结果。

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>



void do_something(long t)

{

    int i = 0;

    for(i = 0; i < t; i++)

    {

        for(i = 0; i < t; i++)

        {

            for(i = 0; i < t; i++);

        }

    }

}



int main(void)

{

    pid_t pid;



    // 此时仅有一个进程

    printf("PID before fork() : %d\n", getpid());

    pid = fork();



    // 此时已经有两个进程在同时运行

    pid_t npid = getpid();

    if(pid < 0)

    {

        perror("fork error\n");

    }

    // pid == 0 表示子进程

    else if(pid == 0)

    {

        while(1)

        {

            printf("I am child process, PID is %d\n", npid);

            do_something(1000000);

        }

    }

    // pid >= 0 表示父进程

    else if(pid >= 0)

    {

        while(1)

        {

            printf("I am father process, PID is %d\n", npid);

            do_something(1000000);

        }

    }



    return 0;

}

运行截图

结果分析:

        该程序中有一个fork()来创建子进程,同时用getpid()来获取进程的pid号,然后通过一个忙等待do_something()来延迟,在另外一个终端内输入ps -lf 来查看程序执行情况,通过输出可知,父进程pid = 4773, 子进程pid = 4774, 说明用fork()创建的子进程的pid一般是跟父进程pid紧挨着的。而且无法确定谁先执行,一般来说,系统默认子进程先执行。

        一个程序调用fork函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。

  1. 在上面的多进程程序中利用exec函数,启动另一个程序的执行。用ps命令显示本机运行的所有进程的详细列表,并分析列表中不同进程的内存占用情况。
#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>



void do_something(long t)

{

    int i = 0;

    for(i = 0; i < t; i++)

    {

        for(i = 0; i < t; i++)

        {

            for(i = 0; i < t; i++);

        }

    }

}



int main(void)

{

    pid_t pid;



    // 此时仅有一个进程

    printf("PID before fork() : %d\n", getpid());

    pid = fork();



    // 此时已经有两个进程在同时运行

    pid_t npid = getpid();

    if(pid < 0)

    {

        perror("fork error\n");

    }

    // pid == 0 表示子进程

    else if(pid == 0)

    {

        while(1)

        {

            printf("I am child process, PID is %d\n", npid);

            execl("/bin/ls","ls","-1","-color",NULL);  

            printf("exec fail!\n");

            

            do_something(1000000);

        }

    }

    // pid >= 0 表示父进程

    else if(pid >= 0)

    {

        while(1)

        {

            printf("I am father process, PID is %d\n", npid);

            do_something(1000000);

        }

    }



    return 0;

}

运行截图:

结果分析:

        父进程pid = 6028, 子进程pid = 6029,  子进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码。子进程开始执行语句execl("/bin/ls","ls","-1","-color",NULL); 即ls语句,罗列当前目录下的信息,在另外一个终端用ps -lf 命令看出子进程利用exec函数,启动另一个程序(ls命令)的执行。

        在Linux中要使用exec类的函数来启动另一程序的执行,exec类的函数不止一个,但大致相同,在Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp。一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息)。那么如果我的程序想启动另一程序的执行但自己仍想继续运行的话,怎么办呢?那就是结合fork与exec的使用。

  1. 编写一个最简单的多线程程序。理解多线程的运行和输出情况;
#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<pthread.h>

void thread(void)

{

    int i;

    for(i=0;i<3;i++)

    {

        printf("This is a pthread.\n");

        

    }

        

}



int main(void)

{

    pthread_t id;

    int i,ret;

    ret=pthread_create(&id,NULL,(void *) thread,NULL);

    if(ret!=0){

        printf ("Create pthread error!\n");

        exit (1);

    }

    //sleep(0.1);

    for(i=0;i<3;i++)

    {

        printf("This is the main process.\n");

        

    }

        

    pthread_join(id,NULL);

    return (0);

}

运行截图:

 

结果分析:

        每次执行程序,发现结果都是一样的,先运行父进程然后在执行子线程,跟实验指导书上的结果不一样,为什么呢?因为在创建子线程时需要时间,而创建完后,由调度策略决定是先执行子线程还是父进程。初步分析是由于CPU频率太快,一会儿就执行完成了,所以每次都是父进程执行完才到子线程执行。修改sleep(0.1)使父进程睡眠,来测试是否符合结果。

运行截图:

 

结果分析:

        通过不断调整父进程中的sleep(0.1)值,来测试子线程的运行情况,发现真的如初步分析结果那样。当然也可以用信号量机制来调整情况。

        多线程和进程相比,它是一种非常"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

  1. 利用信号量机制控制多线程的运行顺序,分析多线程中数据的共享情况;
#include <stdio.h>

#include <pthread.h>

#include <semaphore.h>



#define MAXSTACK 100



int stack[MAXSTACK][2];

int size=0;

sem_t sem;

/* 从文件1.dat读取数据,每读一次,信号量加一*/

void ReadData1(void)

{

    FILE *fp=fopen("1.dat","r");



    while(!feof(fp))

    {

        fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);

        sem_post(&sem);

        ++size;

    }



    fclose(fp);

}



/*从文件2.dat读取数据*/

void ReadData2(void)

{

    FILE *fp=fopen("2.dat","r");

    while(!feof(fp))

    {

        fscanf(fp,"%d %d",&stack[size][0],&stack[size][1]);

        sem_post(&sem);

        ++size;

    }



    fclose(fp);

}



/*阻塞等待缓冲区有数据,读取数据后,释放空间,继续等待*/

void HandleData1(void)

{

    while(1)

    {

        sem_wait(&sem);

        printf("Plus:%d+%d=%d\n",stack[size][0],stack[size][1],

        stack[size][0]+stack[size][1]);

        --size;

    }

}



void HandleData2(void)

{

    while(1)

    {

        sem_wait(&sem);

        printf("Multiply:%d*%d=%d\n",stack[size][0],stack[size][1],

        stack[size][0]*stack[size][1]);

        --size;

    }

}



int main(void)

{

    pthread_t t1,t2,t3,t4;

    sem_init(&sem,1,1);



    pthread_create(&t1,NULL,(void *)HandleData1,NULL);

    pthread_create(&t2,NULL,(void *)HandleData2,NULL);

    pthread_create(&t3,NULL,(void *)ReadData1,NULL);

    pthread_create(&t4,NULL,(void *)ReadData2,NULL);



    /* 防止程序过早退出,让它在此无限期等待*/

    pthread_join(t1,NULL);

}

运行截图:

 

 

结果分析:

在Linux下,用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ,我们运行sem。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。

从中我们可以看出各个线程间的竞争关系。而数值并未按我们原先的顺序显示出来,这是由于size这个数值被各个线程任意修改的缘故。同时每次运行结果都不一样,跟系统调度有密切关系,这也往往是多线程编程要注意的问题。

  1. 分析Linux系统下多进程与多线程中的区别。

现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。

     使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。


  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。


  除了以上所说的优点外,多线程程序作为一种多任务、并发的工作方式,还有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

  • 实验思考题
  1. 多进程并发执行时各个进程的内存分配情况如何?如何监测其分配情况?

Linux下一个进程在内存里有三部份的数据,就是“数据段”,“堆栈段”和“代码段”,一个程序调用fork函数,系统就为一个新的进程准备了前述三个段,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。

采用以下命令可以监测内存分配情况:top, free, vmstat, /proc/meminfo, /proc/<pid>/statm, /proc/<pid>/status

  1. 多线程中数据是如何共享的?

运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,用信号量机制来控制数据的共享,只有当信号量值大于0时,才能使用公共资源,共享资源存放在一个临界区。

在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。

  1. Linux系统下多进程与多线程中的区别是什么?

多线程和多进程相比,它是一种非常“节俭”的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种“昂贵”的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右。

多线程和多进程相比,线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。

 除了以上所说的优点外,多线程程序作为一种多任务、并发的工作方式,还有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

  • 补充实验
  1. 用fork写一个测试程序,从父进程和子进程中打印出各自的PID,并把wait(), exit()系统调用加进去,使子进程返回退出状态给父进程,并将它包含在父进程的打印信息中。对运行结果进行分析,并通过dmesg命令查看系统创建进程过程。
#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<sys/types.h>

#include<wait.h>



int main( )

{      

    int pid;    

    pid = fork();         /*创建子进程*/



    pid_t npid = getpid();



    switch(pid)

    {

        case  -1:                          /*创建失败*/

                printf("fork fail!\n");

                exit(0);

        case  0:                                 /*子进程*/

                

                printf("I am child process, my PID is %d!\n", npid);

                exit(0);

        default:                                 /*父进程*/

                wait(NULL);                  /*同步*/

                printf("I am father process, my PID is %d!\n", npid);

                exit(0);

    }

    

    return 0;

}

 

 

  1. 用sublime命令查看自己计算机上的内核源代码中的include/linux/sched.h 文件中的task_struct 结构,列出类型为list_head 的字段,并说明哪几个字段可以形成进程的树结构,通过pstree 命令查看自己机子上的进程树结构。

struct list_head                rcu_node_entry;

struct list_head                rcu_tasks_holdout_list;

struct list_head                trc_holdout_list;

struct list_head                tasks;

struct list_head                children;

struct list_head                sibling;

struct list_head                ptraced;

struct list_head                ptrace_entry;

struct list_head                thread_group;

struct list_head                thread_node;

struct list_head                cg_list;

struct list_head                pi_state_list;

struct list_head                perf_event_list;

 

 

 

  1. 在Linux中用nproc或者lscpu 命令来查看CPU 核心数和CPU内的存储层次结构,多个核心数就意味着系统可以在同一时间处理多个进程。

 

 

4、在Linux中用top或者uptime 命令来查看系统负载情况,返回的load average的3个数字表示的系统在1分钟、5分钟、15分钟内平均负载情况。

 

  • 4
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值