Unix/Linux操作系统分析实验二 内存分配与回收:Linux系统下利用链表实现动态内存分配
Unix/Linux操作系统分析实验三 文件操作算法: 实现在/proc目录下添加文件
Unix/Linux操作系统分析实验四 设备驱动: Linux系统下的字符设备驱动程序编程
本文章用于记录自己所学的内容,方便自己回顾复习。
实验内容
- 利用fork函数编写一个简单的多进程程序,用ps命令查看系统中进程的运行状况,并分析输出结果。
- 在新创建的子进程中,使用exec类的函数启动另一程序的执行;分析多进程时系统的运行状态和输出结果;
- 利用最常用的三个函数pthread_create,pthread_join和pthread_exit编写了一个最简单的多线程程序。理解多线程的运行和输出情况;
- 利用信号量机制控制多线程的运行顺序,并实现多线程中数据的共享;
- 分析Linux系统下多进程与多线程中的区别;
- 编写程序实现进程的管道通信。用系统调用pipe( )建立一管道,二个子进程P1和P2分别向管道各写一句话:
Child 1 is sending a message!
Child 2 is sending a message!
父进程从管道中读出二个来自子进程的信息并显示(要求先接收P1,后P2)。
7.编写一个HelloWorld内核模块,并进行装载和卸载操作。
实验步骤:
内容一:利用fork函数编写一个简单的多进程程序,最后使用ps命令查看系统中进程的运行状况。
PID:进程标识符 TTY:设备终端号 TIME:进程的运行时间 CMD:执行程序的参数和命令
分析输出结果:父进程调用fork系统调用函数来创建一个子进程,fork函数返回0时,说明子进程在执行;返回子进程的PID时,说明父进程在执行。
内容二:使用父进程调用fork()创建一个子进程,通过调用exec()来用新的程序(输出/bin/ls路径下的所有文件)代替子进程的内容,然后可以调用wait()来控制进程执行顺序,子进程输出/bin/ls路径下的所有文件,父进程输出语句ls complete !。
分析多进程时系统的运行状态和输出结果:
输入top命令查看系统运行状态和进程运行状态:
第一行说明:
top – :系统当前时间
up:服务器连续运行的时间,笔者见过有服务器连续运行一年以上,linux服务器还是非常稳定的。
user:当前有多少用户登录系统
load average:这个边有3个数值分别表示系统在前1分钟,5分钟,15分钟的工作负载,根据笔者以往的经验来看单核负载在3-5之间比较合适,经常在1以下,说明cpu利用率不高,在5以上,cpu会处于较高负载状态,会容易宕机。有一次项目上线,晚上加班观察服务器状况,这个值长时间保持在72左右,因为服务器有八核,所以每核的值为9,后来服务器就挂了。
第二行就是显示任务的数量情况,其中zombie要注意一下,这个是表示僵尸进程,出现了僵尸进程要注意下僵尸进程是如何产生的。如果不找到产生原因,即使杀死了,可能也会再次出现。
第三行表示cpu的运行情况,按下1可以显示每个核的运行情况。
第四行表示内存memory的使用情况。
第五行表示交换空间swap的使用情况。
进程的运行状态,每个表头表示的含义如下:
PID:进程编号
USER:进程所属用户
PR/NI:Priority/Nice value进程执行的优先顺序
VIRT:Virtual Image (kb) 虚拟内存使用总额
RES:Resident size (kb) 常驻内存
SHR:Shared Mem size (kb) 共享内存
S:Process Status 进程状态
%CPU:cpu使用率
%MEM:内存使用率
TIME+:进程开始运行时使用cpu的总时间
COMMAND:进程运行的命令
在top状态下按f可以查看表头字段说明。
内容三:调用函数pthread_create来创建三个线程(在创建一个线程后,可以调用pthread_exit函数使当前线程终止并返回一个值,该值可由函数pthread_join()获取),如果函数返回值不为0,则说明创建线程失败,直接退出程序。调用函数pthread_join来等待所有线程结束,函数返回值不为0,则说明还有线程没有退出;打印相应的信息,退出程序。
理解多线程的运行和输出情况;
(1)运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
(2)线程间方便的通信机制。由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
内容四:创建4个线程,其中两个线程(皆以读方式打开文件1.dat)负责从文件读取数据到公共的缓冲区,另外两个线程(皆以写方式打开文件(2.dat)从缓冲区读取数据作不同的处理(加和乘运算),利用信号量机制控制多线程的运行顺序,并实现多线程中数据的共享。
内容五:分析Linux系统下多进程与多线程中的区别;
答:多进程和多线程的主要区别是:线程是进程的子集(部分),一个进程可能由多个线程组成。多进程的数据是分开的、共享复杂,需要用IPC,但同步简单;多线程共享进程数据,共享简单,但同步复杂。
内容六:用系统调用pipe( )建立一个无名管道,二个子进程P1和P2分别向管道各写一句话:
Child 1 is sending a message!
Child 2 is sending a message!
最后父进程从管道中读出二个来自子进程的信息并显示(要求先接收P1,后P2)。
内容七:首先编写一个HelloWorld.c文件,最后将内核模块进行装载(命令:insmod HelloWorld.ko)和卸载(命令:rmmod HelloWorld)操作。
实验结果分析(截屏的实验结果,与实验结果对应的实验分析)
内容一:
内容二:
内容三:
内容四:
内容六:
内容七:
实验总结:
遇到的问题:在进行内容五、分析Linux系统下多进程与多线程中的区别:编译源文件成功,运行时出现如下错误:
通过查询资料发现:段错误的原因是源文件内存的大小超过了Ubuntu所在段的大小,所以在程序执行的过程中运行到相关的步骤时就会出现段错误(核心已转储)的提示。
解决方法:
在命令行输入命令:ulimit -a(查看Ubuntu当前栈空间的大小)
输入命令:ulimit -s 1024000(将当前栈空间的大小改为100M)
正确结果:
内容七,编写一个HelloWorld内核模块,并进行装载和卸载操作,Makefile的文件名不能为MakeFile,否则会出现以下情况:
实验总结
通过这次实验,我理解了进程、线程的结构和学会创建新进程和新线程的方法,了解在Linux系统中多进程和多线程的区别,了解管道的类型及其建立方法、学会如何使用进程来通过无名管道通信,熟悉内核模块正确的编写规则和方法、理解内核模块的装载和卸载操作,并且能够熟练使用GDB调试程序。
所有实验的源代码如下:
1-1.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
int pid = fork(); //返回值是负数,说明子进程创建失败;返回值是0,则说明处于子进程中;返回值是子进程的进程号,则说明处于父进程中。
switch(pid) {
case -1:
printf("fork fail!\n"); //创建子进程失败
case 0:
printf("Return value of the fork function: %d\t Child process in progress!\n", pid); //子进程正在执行
exit(1); //终止子进程的执行
default:
wait(NULL); //父进程等待子进程完成
printf("Return value of the fork function: %d\t Parent process in process!\n", pid); //父进程正在执行
exit(0); //终止父进程的执行
}
}
1-2.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
int main()
{
int pid = fork(); //返回值是负数,说明子进程创建失败;返回值是0,则说明处于子进程中;返回值是子进程的进程号,则说明处于父进程中。
switch (pid) {
case -1:
printf("fork fail!\n"); //创建子进程失败
exit(1); //父进程退出
case 0: //子进程在执行
execl("/bin/ls", "ls", "-1", "-color", NULL); //子进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码。
printf("exec fail!\n");
exit(1); //终止子进程的执行
default: //父进程在执行
wait(NULL); //父进程等待子进程结束后才执行
printf("ls completed !\n");
exit(0); //终止父进程的执行
}
}
1-3.c
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
void thread(void* arg) {
for (int i = 0; i < 3; i++) {
printf("This is a pthread %d.\n", i + 1);
pthread_exit((void*)8); //使线程终止,线程结束会返回一个值,该值可由函数pthread_join()获取
}
}
int main(void* arg) {
pthread_t id; //线程标识号
int ret;
ret = pthread_create(&id, NULL, (void *)thread, NULL); //创建一个线程,并使得该线程执行thread函数
for (int i = 0; i < 3; i++)
printf("This is the main process %d.\n", i + 1);
if(ret!=0){
printf ("Create pthread error!\n"); //创建线程失败
exit (1); //退出程序
}
void* temp;
ret = pthread_join(id, &temp); //用来等待一个线程结束,直到线程退出后才执行下面的代码。
if (ret) {
printf("The pthread is not exit.\n");
return -1;
}
printf("The pthread exits and returns a value %d.\n", (int)temp);
return (0);
}
1-4.c
//在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另外两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
#include<stdio.h>
#include<stdlib.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* arg) {
FILE* fp = fopen("1.dat", "r"); //以读的方式打开文件1.dat
while (!feof(fp)) { //函数feof:若遍历到文件结束符则返回true,否则返回false
fscanf(fp, "%d %d", &stack[size][10], &stack[size][1]);
sem_post(&sem); //增加信号量sem的值
size++; //每读一次,信号量加一
}
fclose(fp); //关闭文件
}
//从文件2.dat读取数据
void ReadData2(void* arg) {
FILE* fp = fopen("2.dat", "r"); //以读的方式打开文件2.dat
while (!feof(fp)) { //函数feof:若遍历到文件结束符则返回true,否则返回false
fscanf(fp, "%d %d", &stack[size][0], &stack[size][1]);
sem_post(&sem); //增加信号量sem的值
size++; //每读一次,信号量加一
}
fclose(fp); //关闭文件
}
//阻塞等待缓冲区有数据,读取数据后,释放缓冲区空间,继续等待
void HandleData1(void* arg) {
while (1) {
sem_wait(&sem); //用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一
printf("Plus:%d+%d=%d\n", stack[size][0], stack[size][1], stack[size][0] + stack[size][1]);
size--; //信号量减一
}
}
//阻塞等待缓冲区有数据,读取数据后,释放缓冲区空间,继续等待
void HandleData2(void* arg) {
while (1) {
sem_wait(&sem); //用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一
printf("Multiply:%d*%d=%d\n", stack[size][0], stack[size][1], stack[size][0] * stack[size][1]);
size--; //信号量减一
}
}
int main(void* arg) {
pthread_t t1, t2, t3, t4;
sem_init(&sem, 0, 0); //初始化信号量sem,第二个参数0表示此信号量只能为当前的所有线程共享,若不为0,则在进程间共享;第三个参数0表示信号量的初始值
pthread_create(&t1, NULL, (void*)HandleData1, NULL); //用来创建一个线程1
pthread_create(&t2, NULL, (void*)HandleData2, NULL); //用来创建一个线程2
pthread_create(&t3, NULL, (void*)ReadData1, NULL); //用来创建一个线程3
pthread_create(&t4, NULL, (void*)ReadData2, NULL); //用来创建一个线程4
//防止程序过早退出,等其它线程结束后,在退出程序
pthread_join(t1, NULL); //用来等待一个线程的结束
}
1-6.c
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include <sys/wait.h>
int pid1, pid2; //存储进程的进程标识符
void main(void* arg) {
int fd[2]; //句柄
char outpipe[100], inpipe[100]; //无名管道的读出端和写入端
pipe(fd); //新建立一个无名管道fd
while ((pid1 = fork()) == -1); //创建子进程1
if (pid1 == 0) {//返回值为0,说明子进程1在运行
lockf(fd[1], 1, 0); //给子进程上锁,第一个参数fd[1]是文件描述符,第二个参数为1表示锁定(0表示解锁),第三个参数表示锁定或解锁的字节数,0表示从文件的当前位置到文件尾
sprintf(outpipe, "Child 1 process is sending message!"); //把字符串放入读出端数组outpipe中
write(fd[1], outpipe, 50); //向管道里写入长为50字节的字符串
sleep(5); //子进程1自我阻塞5秒(即当前进程睡眠/等待/延迟5秒)
lockf(fd[1], 0, 0); //给子进程1解锁,第一个参数fd[1]是文件描述符,第二个参数为1表示锁定(0表示解锁),第三个参数表示锁定或解锁的字节数,0表示从文件的当前位置到文件尾
//__exit() 函数:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构。
exit(0); //子进程1退出(在执行退出之前,会将文件缓冲区中的内容写回文件,即清理I/O缓冲;)
}
else {//返回值大于0,说明父进程在执行
while ((pid2 = fork()) == -1); //创建子进程2
if (pid2 == 0) {//返回值为0,说明子进程2在运行
sprintf(outpipe, "Child 2 process is sending message!");
write(fd[1], outpipe, 50); //向管道里写入长为50字节的字符串,子进程之间发生互斥
sleep(5); //子进程2自我阻塞5秒(即当前进程睡眠/等待/延迟5秒)
lockf(fd[1], 0, 0); //给子进程1解锁,第一个参数fd[1]是文件描述符,第二个参数为1表示锁定(0表示解锁),第三个参数表示锁定或解锁的字节数,0表示从文件的当前位置到文件尾
exit(0); //子进程2退出(在执行退出之前,会将文件缓冲区中的内容写回文件,即清理I/O缓冲;)
}
else {
wait(0); //等待子进程1结束后,才执行以下操作
read(fd[0], inpipe, 50); //从管道里读出长为50字节的字符串
printf("%s\n", inpipe); //输出管道写入端的内容
wait(0); //等待子进程2结束后,才执行以下操作
read(fd[0], inpipe, 50); //从管道里读出长为50字节的字符串
printf("%s\n", inpipe); //输出管道写入端的内容
exit(0); //父进程退出
}
}
}
HelloWorld.c
//任何模块都要包含的三个头文件
#include <linux/init.h> //包含了宏__init和__exit
#include <linux/kernel.h> //包含了常用的内核函数
#include <linux/module.h> //包含了对模块的版本控制
static int __init lkp_init(void) //模块加载函数,当模块被插入到内核时调用它
{
printk("<0>" "Hello World from the kernel space...\n"); //模块加载的时候系统会打印
return 0;
}
static void __exit lkp_cleanup(void) //模块卸载函数,当模块从内核移走时调用它
{
printk("<0>" "Good Bye World! leaving kernel space...\n"); //模块卸载的时候系统会打印
}
module_init(lkp_init); //模块初始化
module_exit(lkp_cleanup); //模块退出
MODULE_LICENSE("GPL"); //模块具有GUN公共许可证
MODULE_AUTHOR("作者");
MODULE_DESCRIPTION("功能描述");
Makefile(注意:Makefile的格式要写对,例如:命名为MakeFile时,使用make命令编译时会出错,具体命名规则可在CSDN或者百度搜索)
obj-m:=HelloWorld.o
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=usr/src/linux-headers-$(LINUX_KERNEL)
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) cleanobj-m:=HelloWorld.o
PWD:=$(shell pwd)
KVER:=$(shell uname -r)
KDIR:=/lib/modules/$(KVER)/build/
all:
$(MAKE) -C $(KDIR) M=$(PWD)
clean:
rm -rf *.o *.mod.c *.mod.o *.ko *.symvers *.order *.a