2023年操作系统实验报告完整版_第1页
2023年操作系统实验报告完整版_第2页
2023年操作系统实验报告完整版_第3页
2023年操作系统实验报告完整版_第4页
2023年操作系统实验报告完整版_第5页
已阅读5页,还剩32页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

操作系统实验报告学号:姓名:指导老师:完毕日期:ﻬ目录实验一ﻩ1实验二ﻩ2实验三 7实验四 10实验五ﻩ15实验六 18实验七 22实验一UNIX/LINUX入门一、实验目的了解UNIX/LINUX运营环境,熟悉UNIX/LINUX的常用基本命令,熟悉和掌握UNIX/LINUX下c语言程序的编写、编译、调试和运营方法。二、实验内容ﻩ熟悉UNIX/LINUX的常用基本命令如ls、who、pwd、ps等。练习UNIX/LINUX的文本行编辑器vi的使用方法熟悉UNIX/LINUX下c语言编译器cc/gcc的使用方法。用vi编写一个简朴的显示“Hello,World!”c语言程序,用gcc编译并观测编译后的结果,然后运营它。三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验程序#include<stdio.h>#include<stdlib.h>intmain(){printf("HelloWorld!\n");return0;}五、实验感想通过第一次室验,我了解UNIX/LINUX运营环境,熟悉了UNIX/LINUX的常用基本命令,熟悉和掌握了UNIX/LINUX下c语言程序的编写、编译、调试和运营方法。实验二进程管理一、实验目的加深对进程概念的理解,明确进程与程序的区别;进一步结识并发执行的实质。二、实验内容(1)进程创建编写一段程序,使用系统调用fork()创建两个子进程。当此程序运营时,在系统中有一个父进程和两个子进程活动。让每一个进程在屏幕上显示一个字符:父进程显示“a“;子进程分别显示字符”b“和字符“c”。试观测记录屏幕上的显示结果,并分析因素。(2)进程控制修改已编写的程序,将每一个进程输出一个字符改为每一个进程输出一句话,再观测程序执行时屏幕上出现的现象,并分析因素。(3)进程的管道通信编写程序实现进程的管道通信。使用系统调用pipe()建立一个管道,二个子进程P1和P2分别向管道各写一句话:Child1issendingamessage!Child2issendingamessage!父进程从管道中读出二个来自子进程的信息并显示(规定先接受P1,再接受P2)。三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验设计1、功能设计(1)进程创建使用fork()创建两个子进程,父进程等待两个子进程执行完再运营。(2)进程控制使用fork()创建两个子进程,父进程等待两个子进程分别输出一句话再运营。(3)进程的管道通信先创建子进程1,向管道写入一句话,子进程1结束后创建子进程2,向管道写入一句话,最后父进程从管道中读出。2、数据结构子进程和管道。3、程序框图五、实验程序(1)进程创建#include<stdio.h>#include<stdlib.h>intmain(){ﻩintpid1,pid2; pid1=fork(); if(pid1<0){printf("Fork1failed!!");} if(pid1==0){printf("b\n");exit(0);}if(pid1>0){wait(NULL); ﻩ pid2=fork();if(pid2<0){printf("Fork2failed!!");}if(pid2==0){printf("c\n");exit(0);}if(pid2>0){wait(NULL);printf("a\n");exit(0);}}}(2)进程控制#include<stdio.h>#include<stdlib.h>intmain(){ intpid1,pid2;ﻩpid1=fork(); if(pid1<0){printf("Fork1failed!!");}ﻩif(pid1==0){printf("Thisischildb\n");exit(0);}if(pid1>0){wait(NULL); ﻩﻩpid2=fork();if(pid2<0){printf("Fork2failed!!");}if(pid2==0){printf("Thisischildc\n");exit(0);}if(pid2>0){wait(NULL);printf("Thisisfathera\n");exit(0);}}}(3)进程的管道通信#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<memory.h>intmain(){ﻩintpid1,pid2;intpfd[2];ﻩchar*msg1="Child1issendingamessage!"; char*msg2="Child2issendingamessage!";charbuf[256];intr,w;if(pipe(pfd)<0){printf("pipecreateerror!\n");exit(1);} pid1=fork();ﻩif(pid1<0){printf("Fork1failed!!");}if(pid1==0){close(pfd[0]);//writesleep(3);if(w=write(pfd[1],msg1,strlen(msg1))<0){printf("wirteerror!\n");exit(1);}else{printf("child1sendmsgtopipe!\n");}exit(0);}if(pid1>0){wait(NULL);pid2=fork();if(pid2<0){printf("Fork2failed!!");}if(pid2>0){close(pfd[1]);//readsleep(3);if(r=read(pfd[0],buf,256)<0){printf("readerror!\n");exit(1);}else{printf("parentreadfrompipe:%s\n",buf);}wait(NULL);close(pfd[1]);//readsleep(3);if(r=read(pfd[0],buf,256)<0){printf("readerror!\n");exit(1);}else{printf("parentreadfrompipe:%s\n",buf);}}if(pid2==0){close(pfd[0]);//writesleep(6);if(w=write(pfd[1],msg2,strlen(msg2))<0){printf("writeerror!\n");exit(1);}else{printf("child2sendmsgtopipe!\n");}exit(0);}}}六、实验结果ﻩ从图中可以看出,分别输出了,三个字母,三句话,实现了子进程向管道写数据,父进程从管道中读出。七、实验感想通过本次实验,我们对进程的概念加深了理解,熟悉了进程的创建方法与作用机制,明确了进程与程序的异同。同时,我们掌握了使用管道通信的机制,进一步结识了并发执行的实质。实验三一个进程启动另一个程序的执行一、实验目的编写Linux环境下,fork()与exec()的结合使用实现一个进程启动另一个程序的执行的基本方法,掌握exec()的几种调用方法。二、实验内容父进程从终端读取要执行的命令,并交给子进程执行。父进程等待子进程结束,并打印子进程的返回值。三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验设计1、功能设计根据实验规定,父进程和子进程将被分派不同的任务:父进程从终端读取要执行的命令交给子进程,然后等待子进程技术打印子进程的返回值;子进程执行父进程读取的命令并返回给父进程。由于子进程执行命令需要打开其他文献,所以需要使用exec()类的函数实现在一个进程来启动另一个程序,在这里应使用execlp()函数从PATH环境变量中查找文献并执行。对于父进程,可以调用fget()从终端读取要执行的命令,以字符串形式存储然后交给子进程执行,待子进程执行完毕后接受子进程返回值并打印。由于规定带参数的命令也可以执行,那么需要在父进程中fets两次,第一次gets得到命令给串command,然后gets参数给串command1,并且将这两个参数分别传到子进程的exec()函数,其中前两个都是command,第三个是command1。2、数据结构用char*型全局变量command存放指令用char*型全局变量command1存放参数3、程序框图五、实验程序#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>charcommand[256];charcommand1[256];intmain(){ intrtn; //子进程的返回数值ﻩinterrorno;ﻩwhile(1){ printf(">");ﻩﻩﻩﻩﻩ //从终端读取要执行的命令ﻩfgets(command,256,stdin); printf("<"); ﻩﻩ//从终端读取要执行命令的参数ﻩfgets(command1,256,stdin);ﻩcommand[strlen(command)-1]='\0';ﻩcommand1[strlen(command1)-1]='\0';ﻩif(fork()==0){ ﻩ //子进程执行此命令 errorno=(command1[0]==0)?:execlp(command,command,NULL,NULL),ﻩexeclp(command,command,command1,NULL);//假如没有参数则前者的execlp()函数, 假如有参数则执行后者,有无参数只需看command1[0]是否为0ﻩperror(command);//假如exec函数返回,表白没有正常执行命令,打印错误信息 exit(errorno);ﻩ}ﻩelse{ //父进程,等待子进程结束,并打印子进程的返回值ﻩwait(&rtn);ﻩprintf("childprocessreturn%d\n",rtn); } } return0;}六、实验结果由图可知,执行了ls命令,参数为-a,文献按字母顺序显示如上图所示,输入命令ls,输入参数-a,输出文献夹的内容。七、实验感想通过本次实验,我学会了如何用execlp()函数达成一个进程启动此外一个程序的目的。并且对execlp()的几个参数的作用有了一定了解,别且对于fork()和exelp()联合使用的方法和功能。同时,我们进一步熟悉了LINUX系统操作环境,学习了在LINUX环境下编译和调试程序的技巧。 实验四基于消息队列和共享内存的进程间通信一、实验目的Linux系统的进程通信机构(IPC)允许在任意进程间大批量地互换数据。本实验的目的是了解和熟悉:1.Linux支持的消息通信机制及其使用方法2.Linux系统的共享存储区的原理及使用方法。二、实验内容1.消息的创建、发送和接受使用消息调用msgget()、msgsnd()、msggrev()、msgctrl()编制长度为1K的消息的发送和接受程序。2.共享存储取得创建、附接和断接使用系统调用shmget()、shmat()、shmctl()、shmctl(),编制一个与上述功能相同的程序。三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验设计1、消息的创建、发送和接受(1)功能设计为了实现进程之间消息的创建、发送和接受,一方面应定义两个子进程,Server进程负责发送,Client进程负责接受,父进程负责创建。另一方面需要用到msgget()、msgsnd()、msggrev()、msgctrl()等函数进行对消息的控制。题目规定消息长度为1K,那么msgsnd(id,msgp,size,flag)和msgrcv(id,msgp,size,type,flag)函数中参数size应设为1024,msgget(key,flag)中的key应为75。父进程获得创建消息后,子进程Server先后发送编号为1~10的10条消息,子进程Client先后接受这10条消息,方能达成实验目的。(2)数据结构消息(mymsg):结构体实现,包含的成员变量有消息类型和消息内容,具体实现如下:structmymsg{ﻩﻩﻩﻩﻩ //消息的结构体声明longintmymsgtype; ﻩ //消息类型inttext;ﻩ}; ﻩ ﻩ //消息内容(3)程序框图ﻩﻩﻩﻩ ﻩ ﻩ ﻩﻩ ﻩﻩ ﻩ2、共享存储区的创建、附接和断接(1)功能设计为了实现进程通过共享存储区进行通信,需要创建两个进程并且调用shmget()、shmat()、shmctl()函数实现共享存储区的创建、附接和断接。由于共享存储区的写入和读取由两个子进程完毕,而共享存储区在本程序中为所有进程共用的,因此共享存储区的创建、附接和断接均需要在父进程中完毕。具体的实现方式是现在父进程中创建一块共享存储区,然后用int类型指针list指向该存储区的地址;接着创建两个子进程,第一个子进程通过list指针实现向共享存储区写入int类型的数据,第二个子进程通过list指针实现从共享存储区读出int类型的数据。由于两个子进程同时使用了list指针,所以需要控制两个进程互斥,在读进程序中添加了sleep(1)语句。(2)数据结构通过shmid=shmget(key,size,flag)函数建立(获得)共享存储区,返回该共享存储区的描述符shmid。(3)程序框图五、实验程序1、消息的创建、发送和接受#include<stdio.h>#include<sys/types.h>#include<unistd.h>#include<sys/ipc.h>#include<sys/msg.h>#include<sys/shm.h>#defineMAX1024#defineKEY75structmymsg{ ﻩ //消息结构体longintmymsgtype;ﻩﻩ//消息类型inttext; ﻩ ﻩ//消息内容}msg;intmain(){ pid_tpids;pid_tpidc;intmsgid;inti=1; msgid=msgget(KEY,0666|IPC_CREAT);//获得一个消息的描述符 if((pids=fork())==0){ﻩ//创建Server子进程ﻩ while(i<11){msg.mymsgtype=11-i; ﻩﻩ msg.text=i; printf("thesendedmessageis%dth\n",i);i++; msgsnd(msgid,&msg,MAX,0);//向msgid指定的消息队列发送消息,长度为1K} exit(0);}ﻩelse{i=10; if((pidc=fork())==0){//创建Client子进程 while(i!=1){ msgrcv(msgid,&msg,MAX,0,0);//从msgid指定的消息队列接受消息 printf("themessageis%dth\n",msg.text); i=msg.mymsgtype;}ﻩﻩexit(0);}else{ﻩwait(0);wait(0);exit(0);}}}2、共享存储区的创建、附接和断接#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/types.h>#include<sys/ipc.h>#include<sys/shm.h>#defineMAX11#defineKEY75main(){ﻩinti,child1,child2,running=1;//定义子进程号ﻩintid;int*list;ﻩ ﻩﻩﻩ id=shmget(KEY,sizeof(int)*MAX,IPC_CREAT|0666);//建立一块共享存储区,返回该共享存储区的描述符idﻩlist=(int*)shmat(id,0,0);//将list指针指向共享存储区 if((child1=fork())==-1){ printf("errorinforka\n");exit(1);} if(child1==0){sleep(1);ﻩ for(i=0;i<=10;i++) ﻩﻩprintf("yourmessageis:%d\n",list[i]);exit(0);} else{ ﻩif((child2=fork())==-1){printf("errorinforka\n");exit(1);}ﻩ if(child2==0){ﻩi=0;ﻩﻩ while(1){ﻩlist[i]=i;//向缓冲区里写入数据 printf("themessagesentis:%d\n",list[i]);ﻩ ﻩif(list[i]==10)break;i++;}ﻩ ﻩexit(0);} ﻩelse{ wait(0);wait(0);shmdt(list);//将共享存储区与进城断开 ﻩshmctl(id,IPC_RMID,0);//将共享存储区标志为被销毁的 exit(0);}}}六、实验结果(1)消息的创建、发送和接受第一个子进程一次发了10条消息,第二个子进程一次接受了10条消息,消息队列先进先出。(2)共享存储区的创建、附接和断接第一个子进程向共享存储区写入了11个数据,第二个子进程从共享存储区读取,两个进程之间是互斥执行的。七、实验感想 通过本次实验,我学会了如何用消息队列和共享内存的方式实现进程间的通信,掌握了Linux系统的消息通信机制和共享存储区的原理,并在实践过程中掌握了它们的使用方法。在编程和调试的过程中,我进一步熟悉了LINUX环境下的编译过程和调试方法。实验五运用信号实现进程间通信一、实验目的学习UNIX类操作系统信号机制,编写Linux环境下运用信号实现进程间通信的方法,掌握注册信号解决程序及signal()调用方法。二、实验内容编写一个程序,完毕下列功能:实现一个SIGINT信号的解决程序,注册该信号解决程序,创建一个子进程,父子进程都进入等待。SIGINT信号的解决程序完毕的任务涉及打印接受到的信号的编号和进程PID。编译并运营该程序,然后在键盘上敲Ctrl+C,观测出现的现象,并解释。提醒:参见“五、补充材料”中的signal()的基本用法。三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验设计1、功能设计本实验规定运用信号实现进程间通信。为了实现实验目的,需要实现一个SIGINT信号的解决函数func()并注册该信号解决函数。信号解决函数需要完毕的任务应涉及打印接受到的信号的编号和进程的PID,所以func()函数应能实现打印当前进程的pid以及根据接受到的信号的编号打印出该信号名称的功能。父进程和子进程分别接受两个不同的信号,因此两个进程调用signal()函数时应为不同的信号注册信号解决函数。本次实验采用了SIGINT和SIGUSR1两个信号。为了让父子进程都可以接受到信号,在注册信号解决函数后应让两个进程进入等待,并且为了方便验证实验结果,让父进程接受SIGINT信号,子进程接受SIGUSR1信号。2、数据结构信号(SIGNAL):UNIX系统具有20种信号。可以使用signal()函数为每个信号注册信号解决函数。3、程序框图五、实验程序#include<stdio.h>#include<stdlib.h>#include<signal.h>#include<unistd.h>voidfunc(inti);intpid;intmain(){ﻩif((pid=fork())==0) //创建子进程 {ﻩﻩprintf("sonpid:%d\n",pid);//打印子进程号 signal(SIGUSR1,func);//注册SIGUSR1的信号解决程序ﻩ for(;;) //子进程进入等待 ﻩpause(); } else{ﻩ printf("fatherpid:%d\n",pid);//打印父进程号ﻩ signal(SIGINT,func); ﻩ//注册SIGINT的信号解决程序ﻩﻩfor(;;) //父进程进入等待 pause();}}voidfunc(inti)//信号解决函数{ﻩprintf("pid:%d\n",pid);ﻩif(i==SIGUSR1) printf("receivedSIGUSR1\n");ﻩelseif(i==SIGUSR2)ﻩprintf("receivedSIGUSR2\n"); elseif(i==SIGINT) {ﻩﻩprintf("receivedSIGINT%d\n",i);ﻩﻩexit(1);ﻩ}ﻩexit(0);}六、实验结果如图所示,第一次执行后打印父进程和子进程pid,按ctrl+c后,父进程接受信号,信号解决程序输出父进程号2472和信号名称。第二次使用./shiyan5&后台运营此程序,使用kill–USR12472时,子进程接受信号,信号解决程序输出子进程号0,并输出信号名称。七、实验心得通过本次实验,我掌握了注册信号解决程序及signal()调用方法,并且了解了如何让程序在后台运营的方法。我也进一步了解了LINUX系统中进程同步与通信的原理。实验六线程的创建一、实验目的编写Linux环境下的多线程程序,了解多线程的程序设计方法,掌握最常用的三个函数pthread_create,pthread_join和pthread_exit的用法。二、实验内容1、主程序创建两个线程myThread1和myThread2,每个线程打印一句话。使用pthread_create(&id,NULL,(void*)thread,NULL)完毕。 2、创建两个线程,分别向线程传递如下两种类型的参数ﻩ传递整型值 传递字符三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验设计1、创建两个进程每个进程打印一句话(1)功能设计题目规定创建两个线程,每个线程打印一句话,可以认为两个线程的功能是相同的,故只需要写一个线程的运营函数thread(),在这个函数里有一个printf输出一句话即可。然后在main函数里分别创建两个线程,然后等待两个线程结束。(2)数据结构线程:使用pthread_create()创建。每个线程有相应的线程标示符,也有各自的属性。线程可以和线程运营函数绑定,并可以在创建线程时拟定该线程运营函数的参数。(3)程序框图2、创建两个进程每个进程打印一句话分别向线程传递如下两种类型的参数:整型值、字符(1)功能设计题目规定创建两个线程,两个线程分别传递int型和char型数据给线程运营函数。所以要编写两个不同的线程运营函数分别接受int型和char型的数据。相应的pthread_create()函数中要给第四个参数,作为形参传进线程运营函数。(2)数据结构线程:同1,使用pthread_create()创建。每个线程有相应的线程标示符,也有各自的属性。线程可以和线程运营函数绑定,并可以在创建线程时拟定该线程运营函数的参数。(3)程序框图五、实验程序1、创建两个进程每个进程打印一句话#include<stdio.h>#include<stdlib.h>#include<pthread.h>voidthread()ﻩﻩ//线程运营函数{ﻩprintf("Thisisapthread.\n");ﻩ//输出一句话}intmain(){ pthread_tid1,id2;ﻩ//定义两个线程标记符 inti,ret; ﻩﻩ ﻩret=pthread_create(&id1,NULL,(void*)thread,NULL);//创建线程标记为id1if(ret!=0){ﻩ //线程创建失败printf("Createpthreaderror!\n"); ﻩﻩexit(1);}ret=pthread_create(&id2,NULL,(void*)thread,NULL);//创建线程标记为id2if(ret!=0){ﻩﻩ//线程创建失败ﻩﻩﻩprintf("Createpthreaderror!\n");ﻩ exit(1); ﻩﻩ} printf("Thisisthemainprocess.\n");ﻩpthread_join(id1,NULL); //等待第一个线程结束ﻩpthread_join(id2,NULL); //等待第二个线程结束 return(0);}2、创建两个进程每个进程打印一句话分别向线程传递如下两种类型的参数:整型值、字符#include<pthread.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>voidthreadchar(char*c) ﻩ//接受字符的线程运营函数{printf("receiveachar:%c\n",c);}voidthreadint(int*i) ﻩ//接受整数的线程运营函数{printf("receiveaint:%d\n",i);}intmain(void){pthread_tid1,id2;intret;charc='t'; char*a=c;ﻩ ﻩ//定义char*指针变量传参数用ret=pthread_create(&id1,NULL,(void*)threadchar,a); //创建线程1,第四个参数为char*型变量用来传递字符if(ret!=0){ﻩ//线程创建失败 printf("Createpthreaderror!\n");ﻩ ﻩexit(1); }inti=99;int*b=i;ﻩ //定义int*指针变量传参数用ret=pthread_create(&id2,NULL,(void*)threadint,b);ﻩ//创建线程2,第四个参数为int*型变量用来传递字符if(ret!=0){//线程创建失败ﻩ ﻩprintf("Createpthreaderror!\n"); ﻩexit(1);ﻩ ﻩ}printf("Thisisthemainprocess.\n");pthread_join(id1,NULL);//等待线程1结束pthread_join(id2,NULL); //等待线程2结束return(0);}六、实验结果由图可知两个线程主程序创建了两个进程这两个进程分别输出了一句话。主程序分别创建了两个线程并向线程1传递了‘t’向线程2传递了99,线程运营函数分别输出告知接受了这两个参数。七、实验感想通过本次实验,我学会了如何使用LINUX下的线程创建函数pthread_create()来创建线程,并且向线程传递参数。同时更加纯熟的使用LINUX。实验七运用信号量实现进程的控制一、实验目的学习UNIX类操作系统信号量机制,编写Linux环境下运用信号量实现进程控制的方法,掌握相关系统调用的使用方法。二、实验内容创建4个线程,其中两个线程负责从文献读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的解决(加和乘运算)。使用信号量控制这些线程的执行。三、实验规定按照规定编写程序,放在相应的目录中,编译成功后执行,并按照规定分析执行结果,并写出实验报告。四、实验设计1、功能设计题目规定创建4个线程,其中两个负责从文献读数据到缓冲区,另两个负责从缓冲区读数据进行加和乘的运算。我对这4个线程进行如下安排,线程1读后线程2才可以读,线程2读了后线程3才可以进行加的运算,线程3加完了后线程4才干进行乘的运算,线程4乘完后线程1才干继续读。故需4个信号量sem1,sem2,sem3,sem4。线程1消费sem1生产sem2,线程2消费sem2生产sem3,线程3消费sem3生产sem4,线程4消费sem4生产sem1,形成一个循环,直到文献结束为止。2、数据结构信号量(semaphore):数据类型为结构sem_t,本质上是一个长整型的数。一共4个公共缓冲区(stack):采用2维数组的方式实现(stack[NUM][2])。数组中的两列分别存储两个文献中的数据

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论