Linux相关实验总结


文章目录

实验2 linux文件系统

  • 实验一

  • 1.创建文件file1,写入字符串“abcdefghijklmn”;
  • 2.创建文件file2,写入字符串“ABCDEFGHIJKLMN”;
  • 3.读取file1中的内容,写入file2,使file2中的字符串内容为“ abcdefghijklmnABCDEFGHIJKLMN ” ;

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <fcntl.h>
int main()
{
int file1,file2;
char str[20]={'0'};

file1 = open("./file1",O_RDWR|O_CREAT,0777);
file2 = open("./file2",O_RDWR|O_CREAT,0777);

write(file1,"abcdefghijklmn",14);   

lseek(file2, 15, SEEK_SET);
write(file2,"ABCDEFGHIJKLMN",14);
    
lseek(file1, 0, SEEK_SET);
read(file1,str,14);

lseek(file2, 0, SEEK_SET);

write(file2,str,14);

close(file1);
close(file2);
return 0;
}

open函数打开两个文件,返回两个文件描述符;

lseek函数来定位已经打开的文件里面光标的位置,第一个file1文件写完十四个字符以后光标在末尾,因此要想重新读出来,需要把光标的位置重新定位在开头;

因为实验要求是小写在前大写在后,所以先写入大写需要将光标定位在第十五个字符的位置;

将file1、file2的光标定位在开头,读出file1内容存入字符数组str,再将str内容存入file2中,关闭两个文件;

0777中第二位、第三位、第四位分别指的是 当前用户、组内用户、组外用户的权限。它们都是读(4)、写(2)、执行(1) 权限,加和
4+2+1=7;


open()系统调用

功能
打开或创建一个文件或设备
头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
返回值
成功时返回新文件描述符
否则返回-1


参数flags说明

功能
(文件访问的模式)
取值说明(/usr/include/fcntl.h)
这三个是常用的
O_RDONLY:只读形式打开
O_WRONLY:只写形式打开
O_RDWR:读写形式打开

O_APPEND:追加模式打开
O_TRUNC :若文件存在且为只读或只写成功打开,则将长度截为0
O_CREAT:若文件不存在则创建之
使用此选项时,需同时说明参数mode,说明文件的存取许可权位
O_EXCL:若同时指定O_CREAT,而文件已经存在,则出错
该参数可测试文件是否存在,如果不存在则创建此文件


参数mode说明

定义新建文件的访问权限
mode参数的说明
这张图片很清晰的知道了各种权限及值的大小


close()系统调用
  • 功能
    关闭文件描述符
  • 头文件
    #include <unistd.h>
  • 函数原型
    int close(int fd);
    +成功:返回0
    失败:返回-1

read()/write()系统调用

read() 功能
从文件描述符读取数据
头文件
#include <unistd.h>
函数原型
ssize_t read(int fd, void *buf, size_t count);
返回值: 读到的字节数,若已到文件尾为0,若出错为-1
write() 功能
将数据写入文件描述符
头文件
#include <unistd.h>
函数原型
ssize_t write(int fd, const void *buf, size_t count);
返回值: 若成功为已写的字节数,若出错为-1


lseek()系统调用

功能
调节读写的偏移量
头文件
#include <sys/types.h>
#include <unistd.h>
函数原型
off_t lseek(int fildes, off_t offset, int whence);
返回值
成功时返回偏移量位置
否则返回-1
whence说明
SEEK_SET: 从文件头开始的偏移量
SEEK_CUR: 从当前位置开始加offset后的偏移值
SEEK_END: 从文件末开始加offset后的偏移值


  • 实验二

  • 编写代码,完成以下功能:
  • 1.创建新文件,该文件具有用户读写权限。
  • 2.采用dup/dup2/fcntl复制一个新的文件描述符,通过新文件描述符向文件写入“class_name”字符串;
  • 3.通过原有的文件描述符读取文件中的内容,并且打印显示;

用open函数得到一个文件描述符,再用dup/dup2/fcntl中的一个调用,将得到的文件描述符复制下来,这个新和旧文件共享共享同一数据结构。如果对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。因此用旧的描述符来读,用新的来写入和定位。

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
    int fd , fdcopy;
    char buf[15]={'0'};
    fd = open("./test.txt", O_RDWR|O_CREAT);
    
    fdcopy=dup(fd);
    
    write(fdcopy,"CLASS_NAME",10);
    
    lseek(fdcopy, 0, SEEK_SET);
    
    read(fd, buf, 10);
    
    printf("%s\n", buf);
    close(fd);
    return 0;
}

dup()系统调用

功能
复制文件描述符
头文件
#include <unistd.h>
函数原型
int dup(int oldfd);
传给该函数一个现有描述符,返回一个新的描述符
新描述符是传给它的描述符的拷贝,即两描述符共享同一数据结构
如果对一个文件描述符执行lseek操作,得到的第一个文件的位置和第二个是一样的。

返回值
成功时返回新文件描述符
否则-1


dup2()系统调用

功能
复制文件描述符
头文件
#include <unistd.h>
函数原型
int dup2(int oldfd, int newfd);
允许规定一个有效描述符( oldfd )和目标描述符( newfd )
目标描述符将变成源描述符的复制品,即两个文件描述符指向同一文件,且是源描述符指向的文件

返回值
成功时返回新文件描述符
否则-1


fcntl()系统调用

功能
根据文件描述符来操作文件的特性
头文件
#include <unistd.h>
#include <fcntl.h>
函数原型
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
参数说明
fd:文件描述符
cmd:操作命令
arg:命令使用的参数
lock:同上
返回值
若成功,则依赖于cmd
若出错为-1
cmd参数说明
F_DUPFD:复制文件描述符
FD_CLOEXEC:设置close-on-exec标志
F_GETFD:读取文件描述符标志
F_SETFD:设置文件描述符标志
F_GETFL:读取文件状态标志
F_SETFL:设置文件状态标志
F_GETLK:获取记录锁
F_SETLK :释放记录锁
F_SETLKW:测试记录锁


  • 实验三

  • 编写程序实现以下功能:
  • 1.输入文件名称,能够判断文件类型,判断实际用户对该文件具有哪些存取权限;
  • 2.要求打印出文件类型信息,inode节点编号,链接数目,用户id,组id,文件大小信息;
  • 3.修改文件的权限为当前用户读写,组内用户读写,组外用户无权限。

首先stat这个结构体可以保存一个文件的属性信息,在声明stat的对象之后,用stat()/fstat()/lstat()系统调用将要获取的文件状态存入声明的结构体。判断存取权限用access系统调用,通过不同的参数来判断不同的存取权限。修改权限,chmod系统调用第二个参数就是新的权限,第一个参数是要修改的文件名字。该名字在argv中。

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc,char* argv[])
{
struct stat statBuf;    
for(int i=0; i<argc; i++)
{
     if(access(argv[i], R_OK)<0)
        perror("access R_OK:");
     else
        printf("user can read %s\n", argv[i]);
        
	if(access(argv[i],W_OK)<0)
	   perror("access W_OK:");
	else
        printf("user can write %s\n", argv[i]);
        
	if(access(argv[i],X_OK)<0)
	   perror("access X_OK:");
	else
        printf("user can write and read %s\n", argv[i]);
        
	if(access(argv[i],F_OK)<0)
	   perror("access F_OK:");
	else
        printf("user can exitct %s\n", argv[i]);
        
	int fd=open(argv[i],O_RDWR|O_CREAT,0777);
		
	fstat(fd, &statBuf);  
	printf("fd UID is:%ld\n", statBuf.st_uid);
	printf("fd GID is:%ld\n", statBuf.st_gid);
	printf("fd inode is:%ld\n", statBuf.st_ino);
	printf("fd size is:%ld\n", statBuf.st_size);
	printf("fd link is:%ld\n", statBuf.st_nlink);
	
	if(S_ISREG(statBuf.st_mode))      printf("%s is regular fileln\n",argv[i]);
	else if(S_ISDIR(statBuf.st_mode)) printf("%s is directory fileln\n",argv[i]);
	else if(S_ISCHR(statBuf.st_mode)) printf("%s is char device fileln\n",argv[i]);
	
	chmod(argv[i],0660);
	
}
 return 0;
}


struct stat结构定义

struct stat {
mode_t st_mode; / * file type & mode * /
ino_t st_ino; / * inode number (serial number) * /
dev_t st_rdev; / * device number (file system) * /
nlink_t st_nlink; / * link count * /
uid_t st_uid; / * user ID of owner * /
gid_t st_gid; / * group ID of owner * /
off_t st_size; / * size of file, in bytes * /
time_t st_atime; / * time of last access * /
time_t st_mtime; / * time of last modification * /
time_t st_ctime; / * time of last file status change * /
long st_blksize; / * Optimal block size for I/O * /
long st_blocks; / * number 512-byte blocks allocated * /
};

这个结构体里面存放文件的属性等相关信息


stat()/fstat()/lstat()系统调用

功能
获取文件状态
头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
函数原型
int stat(const char *file_name, struct stat *buf);
int fstat(int filedes, struct stat *buf);
int lstat(const char *file_name, struct stat *buf);

与stat()差别:为符号连接时,lstat()返回连接自身状态
返回值
成功时返回0
否则-1


access()系统调用

功能
按实际用户ID和实际组ID测试文件存取权限
头文件
#include <unistd.h>
函数原型
int access(const char *pathname, int mode);
返回值
成功时返回0
否则-1
mode参数说明
F_OK 值为0,判断文件是否存在
R_OK 值为4,判断对文件是否有读权限
W_OK 值为2,判断对文件是否有写权限
X_OK 值为1,判断对文件是否有执行权限
(R_OK | W_OK | X_OK: 可以同时使用)


chmod()/fchmod()系统调用

功能
更改文件权限
头文件
#include <sys/types.h>
#include <sys/stat.h>
函数原型
int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);
第一个参数文件名字,第二个参数要修后的新权限
返回值
成功时返回0
失败返回-1


测试文件类型的宏
  • 定义位置
    <sys/stat.h>

    在这里插入图片描述

  • 实验四

  • 编写程序实现以下功能:
  • 1.新建文件,设置文件权限屏蔽字为0;
  • 2.建立该文件的硬链接文件,打印硬链接文件的inode节点号和文件大小;
  • 3.建立该文件的软链接文件,打印软链接文件的inode节点号和文件大小;打印软链接文件中的内容;
  • 4.打印源文件的inode节点号,文件大小和链接数目;
  • 5.调用unlink对源文件进行操作,打印源文件链接数目;

所谓软连接,就相当于我们桌面上的快捷方式,如桌面上QQ的图标就是快捷方式,就相当于一个软连接,所以软连接被删除后源文件还存在;而硬链接不同,硬链接和源文件有同样的数据结构,当硬链接被删除后,源文件也就不存在了。

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc ,char *argv[])
{
umask(0); 
creat("test",S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
int fd = open("test",O_RDWR,0777);
write(fd,"abcdefghijklmn",14);
struct stat statBuf;
unlink("hard_test");
if((link("test","hard_test"))==-1)
{
perror("link");
exit(0);
}
int fd1=open("hard_test",O_RDONLY);
fstat(fd1, &statBuf);
printf("hard_test inode is : %ld \n",statBuf.st_ino);
printf("hard_test size is : %ld \n", statBuf.st_size);

char  buf[1024];
memset(buf,'\0',1024);
unlink("sym_link_test");
if((symlink("test","sym_link_test"))==-1)
{
perror("symlink:");
exit(0);
}
int fd2=open("sym_link_test",O_RDONLY,0777);
fstat(fd2, &statBuf);
printf("sym_link_test inode is : %ld \n",statBuf.st_ino);
printf("sym_link_test size is : %ld \n", statBuf.st_size);
read(fd2,buf,1024);

printf("buf is:\n %s \n",buf);

int fd3=open("test",O_RDONLY);
fstat(fd3, &statBuf);
printf("test inode is : %ld \n",statBuf.st_ino);
printf("test size is : %ld \n", statBuf.st_size);
printf("test link is : %ld \n", statBuf.st_nlink);

unlink("test");
printf("test link is : %ld \n", statBuf.st_nlink);
return 0;
}

umask()系统调用

功能
为进程设置文件存取权限屏蔽字.
头文件
#include <sys/types.h>
#include <sys/stat.h>
函数原型
mode_t umask(mode_t mask);


link()/unlink()系统调用

功能
创建/删除一个硬链接
头文件
#include <unistd.h>
函数原型
int link(const char *oldpath, const char *newpath);
int unlink(const char *pathname);
删除一个文件的目录项并减少它的链接数

创建时候需要保证该硬链接不存在否则创建失败
返回值
成功时返回0
失败返回-1


  • 实验五

  • 1.新建/home/user目录;
  • 2.把当前工作路径移至/home/user目录;
  • 3.打印当前工作路径;

mkdir系统调用后可创建一定权限的目录文件,rmdir则相反;

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
int main()
{
    if(mkdir("/home/user",0777)<0)
    {
        perror("mkdir error:");
    }
    else
    printf("mkdir ok\n");
    char a[]="/home/user";
    chdir(a);
    char c[100];
    getcwd(c,100);
    printf("current working directory:%s\n",c);
    rmdir("/home/user");
    return 0;
}

mkdir()/rmdir()系统调用

功能
创建/删除一个空目录
头文件
#include <sys/stat.h>
#include <sys/types.h>
函数原型
int mkdir(const char *pathname, mode_t mode);
int rmdir(const char *pathname);

返回值
成功时返回0
失败时返回-1


chdir()/fchdir()系统调用

功能
更改工作目录
头文件
#include <unistd.h>
函数原型
int chdir(const char *path);
int fchdir(int fd);
返回值
成功时返回0
失败时返回-1
说明
当前工作目录是进程的属性,所以该函数只影响调用chdir的进程本身


getcwd()系统调用

功能
获得当前工作目录的绝对路径
头文件
#include <unistd.h>
函数原型
char *getcwd(char *buf, size_t size);
返回值
成功时返回buf
出错则为NULL
注意

1、在调用此函数时,buf 所指的内存空间要足够大。若工作目录绝对路径的字符串长度超过参数size 大小,则返回NULL,errno 的值则为ERANGE。
2、倘若参数buf 为NULL,getcwd()会依参数size 的大小自动配置内存(使用malloc()),如果参数size 也为0,则getcwd()会依工作目录绝对路径的字符串长度来决定所配置的内存大小,进程可以在使用完次字符串后利用free()来释放此空间。


  • 实验六

  • 编写程序完成以下功能:
  • 1.递归遍历/home目录,打印出所有文件和子目录名称及节点号。
  • 2.判断文件类型,如果是子目录,继续进行递归遍历,直到遍历完所有子目录为止。

DIR是一个目录文件类型的指针,可以指向一个目录文件;
字符数组p的大小可以改变,根据自己home目录文件的多少来改变。如果空间太少gcc或者g++编译后执行会报段错误,也就是空间不够,这里设置的是1024个空间大小;
我们要跳过隐藏文件的遍历,隐藏文件前面都有个 ‘.’ 的标志,不然会无限循环;
通过获取文件状态中的路径,还有当前目录的文件名字,调用sprintf来组成下一个访问路径。依次遍历;

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<dirent.h>
#include<unistd.h>
void display(char *d)
{
    char p[1024];
    struct stat statBuf;
    DIR *dir;
    struct dirent *dirp;
    if(!(dir=opendir(d)))
    {
      perror("open fail:");
      exit(-1);
    }
    while((dirp=readdir(dir))!=NULL)
    {
        if(dirp->d_name[0]=='.')
        {
            continue;
        }
        sprintf(p,"%s/%s",d,dirp->d_name);
        lstat(p,&statBuf);
        if(S_ISDIR(statBuf.st_mode))
        {
          printf("dir:%s\n",dirp->d_name);
          display(p);
        }
        else
        {
          printf("file:%s\t%ld\n",dirp->d_name,statBuf.st_ino);
        }
    }
}
int main()
{
    printf("/home\n");
    display("/home");
    return 0;
} 


基本数据结构

DIR
目录流对象
头文件
<dirent.h>
定义形式
typedef struct _dirstream DIR;
struct dirent
目录项
头文件
<dirent.h>
定义
ino_t d_ino; /* inode号*/
char d_name[NAME_MAX + 1]; /* 文件名*/


目录基本操作

功能
打开、关闭、读、定位
头文件
#include <sys/types.h>
#include <dirent.h>
函数原型
DIR *opendir(const char *name);
int closedir(DIR *dir);
struct dirent *readdir(DIR *dir);

off_t telldir(DIR *dir); //获取目录流读取位置
void seekdir(DIR *dir, off_t offset);


C 库函数 - sprintf()

功能
C 库函数 int sprintf(char *str, const char *format, …) 发送格式化输出到 str 所指向的字符串。



实验3 进程的管理

  • 实验一

  • 编写代码,实现以下功能:
  • 打印当前所有环境变量的值;
  • 添加新的环境变量NEWENV=first;
  • 修改环境变量NEWENV的值为second;
  • 打印环境变量NEWENV的值。

环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。
环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。
用下面的系统调用便可以完成这些要求。
通过这个循环可以获取当前所有环境变量;

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
extern char **environ;
int main(int argc ,char *argv[])
{
	char** env=environ;
	while(*env)
	{
		printf("%s\n\n\n",*env);
		env++;
	}
	putenv("NEWENV=first");
	setenv("NEWENV","second",1);
	printf("NEWENV=%s\n",getenv("NEWENV"));
	unsetenv("NEWENV");
	return 0;
}

程序代码的组成:

代码段: 代码段中存放的是CPU执行的指令。
数据段:
1.包括初始化数据段(data)和非初始化数据(bss)。
2.初始化数据段包含程序中明确给定初值的全局变量和静态变量。该段在程序运行过程中大小不能发生改变。如:int max= 100;static int count = 10;
3.没有明确给定初值的全局变量和静态变量存放在非初始化数据段中。
栈: 所有的自动变量及函数调用时需要保存的信息(返回地址,函数调用前各寄存器的值)都保存在栈上。
堆: 堆用于存储用户申请的内存空间,常在堆中进行动态内存分配。
环境变量: 提供进程运行所需环境信息。如:shell类型、IP地址、命令搜索路径等。
系统数据: 对进程进行管理、控制所需的信息,如:任务结构体、内核堆栈。


getenv

头文件
#include<stdlib.h>
函数原型
char * getenv(const char *name);
函数说明
getenv()用来取得参数name环境变量的内容。参数name为环境变量的名称,如果该变量存在则会返回指向该内容的指针。环境变量的格式为name=value。
返回值
执行成功则返回指向该内容的指针,找不到符合的环境变量名称则返回NULL。


putenv

头文件
#include<stdlib.h>
定义函数
int putenv(const char * string);
函数说明
putenv函数将形式为name=value的字符串,放入环境表中;
若name已经存在,则先删除其原来的定义。
返回值
执行成功则返回0,有错误发生则返回-1。


设置环境变量setenv

setenv函数原型
int setenv(const char* name, const char* value, int rewrite);
setenv将环境变量name的值设置为value。
若name已经存在
rewrite != 0,该环境变量原已有内容会被改为参数value所指的变量内容。
rewrite == 0,且该环境变量已有内容,则参数value会被忽略。
返回值
执行成功则返回0,有错误发生时返回-1。


删除环境变量

功能
unsetenv函数用于删除某个环境变量
函数原型
int unsetenv(const char* name);


  • 实验三

  • 创建子进程
  • 1.在子进程中打开文件file1,写入自己的“班级_姓名_学号”,
  • 2.父进程读取file1中的内容,并且打印显示。
  • 3.在父进程中获取已经结束的子进程的状态信息,打印该信息,并且打印结束的子进程的进程号。

子进程打开文件写入之后,要关闭。父进程需要再次打开文件,之后再关闭;
fork创建的子进程,其优先级与父进程一样,而vfork所创建的子进程优先级比父进程高。所以父进程要等待子进程,并且获取子进程的退出状态;
fork在父进程中返回子进程的id,在子进程中返回0;
wait在所等待的进程结束后可获取其退出状态,返回其ID

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
#include<fcntl.h>
int main(int argc ,char *argv[])
{
	pid_t pid;
	int status,i,fd,wnum;
	char str[40]={'0'};
	if(fork()==0)
	{
	fd=open("file.txt",O_RDWR|O_CREAT,0777);
	
	wnum = write(fd,"19专业1班_我名字_1915我学号",33);
	close(fd);
	exit(5);
	}
	else{
	sleep(2);
	fd=open("file.txt",O_RDWR|O_CREAT,0777);
	lseek(fd, 0, SEEK_SET);
	read(fd,str,33);
	printf("file.txt is : %s \n",str);
	printf("This is the parent process ,wait for child .. \n");
	pid=wait(&status);
	if(WIFEXITED(status))
	i=WEXITSTATUS(status);
	printf("child's pid =%d . exit status=%d\n",pid,i);
	close(fd);
	}
	return 0;
}

检测退出状态的宏:
  1. WIFEXITED(status)判断子进程是否为正常退出(不是被其它进程杀死),如果是,返回一个非零值.
  2. WEXITSTATUS(status)当WIFEXITED返回非零值时,可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5。如果进程不是正常退出的,即WIFEXITED返回0,这个值就毫无意义。

wait

函数原型:
pid_t wait(int *status)
参数:

  1. status 可以为NULL,表明父进程不需要子进程的终止状态。
  2. 若status不是空指针,则进程终止状态就存放在它指向的存储单元中,由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,就设计了一套专门的宏(macro)来完成这项工作。
    返回值:若成功返回终止进程ID,出错返回-1

  • 实验四

  • 编写程序实现以下功能:
  • 1,在父进程中定义变量n,在子进程中对变量n进行++操作;并且打印变量n的值,打印子进程pid;
  • 2,在父进程中打印变量n的值,并且打印父进程pid。
  • 3,要求分别用fork和vfork创建子进程。

fork和vfork除了下面的区别还有,fork创建的子进程,其优先级与父进程一样,而vfork所创建的子进程优先级比父进程高。

#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include<string.h>
#include <fcntl.h>
int main(int argc ,char **argv)
{
	pid_t pid;  
	int n=9;
	printf("\n\nfork:\n");      
	if(fork()==0){                
	n++;             
	printf("fork: This is the child process .pid =%d\n",getpid());
	printf("fork: child n=%d\n",n);
	exit(5);      
	}
	printf("fork: parent process pid = %d \n",getpid());
	printf("fork: parent n= %d \n",n);

	sleep(1);
	printf("\n\nvfork : \n");
	if(vfork()==0){                
	n++;             
	printf("vfork :This is the child process .pid =%d\n",getpid());
	printf("vfork :child n=%d\n",n);
	exit(5);      
	}
	printf("vfork :parent process pid = %d \n",getpid());
	printf("vfork :parent n= %d \n",n);
	return 0;

}

fork函数

一个进程可以调用fork函数创建一个新进程
新进程被称为子进程
函数原型
pid_t fork(void);
返回值
fork函数调用一次,但是返回两次
在子进程中返回0,在父进程中返回子进程ID,出错返回-1
通过返回值,可以确定是在父进程还是子进程中


vfork函数
  1. vfork与fork的函数原型相同,但两者的语义不同
    vfork用于创建新进程,而该新进程的目的是exec一个新程序(执行一个可执行的文件)
  2. vfork函数并不将父进程的地址空间完全复制到子进程中。
  3. 子进程在调用exec或exit之前,在父进程的地址空间中运行
    vfork函数保证子进程先执行,在它调用exec或者exit之后,父进程才可能被调度执行

进程标识符

获取进程常见标识符
调用进程的进程ID:pid_t getpid();
调用进程的父进程ID:pid_t getppid();
调用进程的实际用户ID:uid_t getuid();
调用进程的有效用户ID:uid_t geteuid();
调用进程的实际组ID:gid_t getgid();
调用进程的有效组ID:gid_t getegid();

查看进程情况(4.8)-命令
$ps -ef | less


  • 实验五

  • 创建子进程一,在子进程中递归打印/home目录中的内容(用exec系列函数调用第二次实验中的代码完成此功能);
  • 1.子进程结束的时候完成以下功能:
  • 打印字符串“Child process exited!”
  • 打印子进程标识符,打印父进程标识符。
  • 2.创建子进程二, 打印子进程运行环境中环境变量“USER”的值,通过exec系列中的某个函数设置子进程”USER”环境变量值为“zhangsan”,并且让该子进程完成以下命令:“ls –li /home”.

exec系列函数都有着共同的功能,就是执行一个可执行的程序,当dishome程序编译完成生成一个可执行程序后,该可执行程序可以作为execl系统调用的参数使用。
当程序执行到execl调用时就会执行dishome程序。
这个实验里要用到sprintf,注意它的使用方法;
execle可以执行一条命令;注意参数的使用。


dishome方法

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<dirent.h>
#include<unistd.h>
void display(char *d)
{
    char p[1024];
    struct stat statBuf;
    DIR *dir;
    struct dirent *dirp;
    if(!(dir=opendir(d)))
    {
      perror("open fail:");
      exit(-1);
    }
    while((dirp=readdir(dir))!=NULL)
    {
        if(dirp->d_name[0]=='.')
        {
            continue;
        }
        sprintf(p,"%s/%s",d,dirp->d_name);
        lstat(p,&statBuf);
        if(S_ISDIR(statBuf.st_mode))
        {
          printf("dir:%s\n",dirp->d_name);
          display(p);
        }
        else
        {
          printf("file:%s\t%ld\n",dirp->d_name,statBuf.st_ino);
        }
        
    }
}
int main()
{
    printf("/home\n");
    display("/home");
    return 0;
} 

gcc -o dishome test.c

#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include<string.h>
#include <fcntl.h>
extern char **environ;
int main(int argc ,char **argv)
{
	pid_t pid;  
	int n;      
	if(fork()==0){                
	
		pid=getpid();
		/*dishome 实验一遍历home文件的可执行程序
dishome在实验一中*/
		if(execl("dishome",NULL)<0)
			perror("execlp error!");
	exit(0);
	}
	
	printf("Child process exited!\n");
	printf("child process .pid =%d\n",pid);
	printf("parent process .pid =%d\n",getpid());     
	

	if(fork()==0)
	{
	char** env=environ;

	printf("process2:USER=%s\n",getenv("USER"));
	char *envp[]={"PATH=/bin","USER=zhanSan",NULL};


                if(execle("/bin/ls","ls","-li","/home",NULL,envp)<0)
                perror("execle error!");
	
	unsetenv("USER");
	exit( 0);
	
	}
	return 0;
}

进程执行函数

#include <unistd.h>
int execl(const char *path, const char *arg, …, NULL);
int execlp(const char *file, const char *arg, …, NULL);
int execle(const char *path, const char *arg, …, NULL ,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
注意:
exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。



实验4 进程间通信

  • 管道实验一

  • 1.编写程序实现以下功能:
  • 2.利用匿名管道实现父子进程间通信,要求
  • 3.父进程发送字符串“hello child”给子进程;
  • 4.子进程收到父进程发送的数据后,给父进程回复“hello farther”;
  • 5.父子进程通信完毕,父进程依次打印子进程的退出状态以及子进程的pid。

匿名管道的主要特点是当程序执行完时,这个匿名管道资源就会被清空,在系统中不可以查看到其具体文件。
管道打开以后,进程间通信read和write就足够。

#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include<string.h>
#include <fcntl.h>
#define BUFSZ 256
int main(void){
        int fd[2];
	   pid_t pid;
        char buf[BUFSZ];
        int len,i;
		int status;
   if( pipe(fd)<0 )
        {
                perror("failed to pipe");
                exit(1);
        }
   if( (pid = fork())<0 ) {
                perror("failed to fork");
                exit(1);
        }

	else if(pid > 0) {
        write(fd[1], "Hello Child\n", 12);
        sleep(1);
		printf("This is child send farther\n");
       	close(fd[1]);
		read(fd[0],buf,BUFSZ);
		printf("%s\n",buf);
		sleep(1);
		pid=wait(&status);
	if(WIFEXITED(status))
		i=WEXITSTATUS(status);
		printf("child's pid =%d . exit status=%d\n",pid,i);
         exit(0);
        }
        else  {
                printf("This is father send child\n");
                read(fd[0], buf, BUFSZ);
			write(fd[1], "Hello father\n", 13);
                printf("%s\n", buf);
			exit(1);
			}
                return 0;
			}


有名/无名管道区别

无名管道: (匿名管道)在系统中没有实名,不能在文件系统中以任何方式看到该管道,它只是进程的一种资源,会随着进程的结束而被系统清除。
有名管道:也称为FIFO管道,是一种文件类型,在文件系统中可以查看到。


匿名管道的建立

基本函数
int pipe(int fd[2]);
参数说明
fd[2]描述管道两端
fd[0]只能用于读,称为管道读端
fd[1]只能用于写,称为管道写端
若试图从写端读,或者向读端写都将导致错误发生

返回值
成功时返回0,失败时返回-1
说明
基本文件I/O函数都可用于管道
如close()、read()、write()等
低层系统调用
sys_pipe( )–>do_pipe()


  • 管道实验二

  • 编写程序实现以下功能:
  • 利用匿名管道实现兄弟进程间通信,要求
  • 兄进程发送字符串“This is elder brother ,pid is (兄进程进程号)”给第进程;
  • 弟进程收到兄进程发送的数据后,给兄进程回复“This is younger brother ,pid is(第进程进程号)”;

这里也要用到sprintf;

#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include<string.h>
#include <fcntl.h>
#define BUFSZ 256
int main(void){
        int fd[2];
	   pid_t pid;
        char buf[BUFSZ];
        if( pipe(fd)<0 )
        {
                perror("failed to pipe");
                exit(1);
        }
        if( (pid = fork())<0 ) {
                perror("failed to fork");
                exit(1);
        }

else if(pid > 0) {
	sleep(1);
	if( (pid = fork())<0 ) {
                perror("failed to fork");
                exit(1);
        }
        else if(pid >0)
		{
			exit(0);
		 } 
		 else {
		 	char str[50]="This is younger brother ,pid is ";
		 	sprintf(buf,"%s%d",str,getpid());
		 	write(fd[1],buf,strlen(buf));
	    	read(fd[0],buf,BUFSZ);
			printf("%s\n",buf);
         exit(0);
		 }
         
        }

  • 管道实验三

  • 编写程序实现以下功能:
  • 利用有名管道文件实现进程间通信,要求
  • 写进程向有名管道文件写入10次“hello world”;
  • 读进程读取有名管道文件中的内容,并依次打印。


#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<limits.h>
#include<fcntl.h>
#define BUFES PIPE_BUF

int main(int argc, char* argv[]) {
	//mode_t mode = 0666;
	mode_t mode = O_NONBLOCK;
	pid_t pid;
	char buf[BUFES];
	int n, i,len,fd;
	if( argc!=2 ) {
		printf("USEMSG: create_fifo {fifoname}\n");
		exit(1);
	}
	if((mkfifo(argv[1],mode))<0) {
		perror("failed to mkfifo");
		exit(1);
	} 
		if((pid = fork())>0) {
		if( (fd=open(argv[1],O_WRONLY)) <0) {
			perror("open");
			exit(1);
		}
		for(i=0; i<10; i++) {
			if ( write(fd,"Hello world\n",12)<0 ) {
				perror("write");
				exit(1);
			}
		}
		exit(0);
		close(fd);
	} else {
		if((fd=open(argv[1],O_RDONLY))<0) {
			perror("open");
			exit(1);
		}
		while((len=read(fd,buf,BUFES))>0)
			printf("%s\n",buf);
		close(fd);
		exit(0);
	}
	return 0;
}

有名管道的建立

基本函数
int mkfifo(const char * pathname, mode_t mode);
参数说明
pathname:创建的FIFO名字
mode:规定FIFO的读写权限
返回值
成功时返回0
失败时返回-1
若路径名存在,则返回EEXIST错误
说明
一般文件的I/O函数都可用于管道,如open(), close(), read(), write()等。


  • 信号实验一

  • 编写程序实现以下功能:
  • 进程A向进程B发送SIGUSR1信号;
  • 进程B收到信号后,打印字符串“receive SIGUSR1”;
  • 要求用kill函数和signal函数实现以上功能;

信号是进程间又一种方式通信方式,通过在进程中安装不同的信号,来执行不同的动作,这里有两种方法来实现上述要求。
首先要知道kill系统调用在这里不是杀死一个进程,kill也具有发送信号的功能。具体方法,在下面。


方法一:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void fun(int sig)
{
	if(sig == SIGUSR1)
	  printf("Reseived SIGUSR1!\n");
}
int main()
{
	int pid;
	
	if(signal(SIGUSR1,fun) < 0)
		perror("signal");
	
	pid = fork();
	
	if(pid < 0)
	  perror("fork");
	else if(pid == 0)
	{
		printf("This is B process!\n");
 
		sleep(2);
	}
	else
	{
		printf("This is A process!\n");
		if(kill(pid,SIGUSR1) < 0)
		  perror("kill");
		return 0;
	}
}



当后台挂起以后,在命令窗口执行kill命令,具体在下面强调字段中。


方法二:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void fun(int sig)
{
	if(sig = SIGUSR1)
		printf("Received SIGUSR1!\n");
}
 
int main()
{
	printf("This is A process,mypid is: %d\n",getpid());
	signal(SIGUSR1,fun);
	pause();
	return 0;	
}


第二个方法,在运行时不用./a.out;而是./a.out&,后面加一个取地址符后可以把该可执行程序作为后台程序执行,并且显示该程序的pid。
然后在命令窗口输入命令 kill -SIGUSR1 (pid)


信号发送函数—kill()

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)
功能
向进程或进程组发送一个信号 (成功返回 0; 否则,返回 -1 )
参数说明
pid:接收信号的进程(组)的进程号
pid>0:发送给进程号为pid的进程
pid=0:发送给当前进程所属进程组里的所有进程
pid=-1:发送给除1号进程和自身以外的所有进程
pid<-1:发送给属于进程组-pid的所有进程
signo:发送的信号

Signo = 0:不发送信号,可用于检查目标进程是否存在,以及当前进程是否具有向目标进程发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。


信号安装函数—signal()

原型定义
void (*signal(int signum, void (*handler)(int)))(int);
参数说明
signum:需要安装的信号
handler:与安装信号相关的处理函数,可以是SIG_IGN或SIG_DFL
SIG_IGN:忽略该信号
SIG_DFL:执行默认操作函数

返回值
成功时,返回新安装信号处理函数handler的返回值
失败时,返回SIG_ERR
底层系统调用
sys_signal(int sig, __sighandler_t handler)


  • 信号实验二

  • 编写程序实现以下功能:
  • 调用setitimer函数分别触发SIGALRM信号,SIGVTALRM信号,SIGPROF信号 ;(可以由多进程分别触发每个信号)
  • 编写信号安装函数,在该函数内部能判断接受到的是什么信号,并把信号打印出来。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/types.h>
void fun(int sig)
{
	if(sig == SIGALRM)
	  printf("Received the SIGALRM!\n");
	else if(sig == SIGVTALRM)
	  printf("Receive the SIGVTALRM!\n");
	else if(sig == SIGPROF)
	  printf("Receive the SIGPROf!\n");
}
 
int main()
{
	if(signal(SIGALRM,fun) < 0)
		perror("signal");
	if(signal(SIGVTALRM,fun) < 0)
	    perror("signal");
	if(signal(SIGPROF,fun) < 0)
	    perror("signal");
 
	struct itimerval new_value1,new_value2,new_value3;
	
	new_value1.it_value.tv_sec = 1;
	new_value1.it_value.tv_usec = 0;
	new_value1.it_interval.tv_sec = 2;
	new_value1.it_interval.tv_usec = 0;
 
	setitimer(ITIMER_REAL,&new_value1,NULL);
	
	new_value2.it_value.tv_sec = 1;
	new_value2.it_value.tv_usec = 0;
	new_value2.it_interval.tv_sec = 2;
	new_value2.it_interval.tv_usec = 0;
 
	setitimer(ITIMER_VIRTUAL,&new_value2,NULL);
 
	new_value3.it_value.tv_sec = 1;
	new_value3.it_value.tv_usec = 0;
	new_value3.it_interval.tv_sec = 2;
	new_value3.it_interval.tv_usec = 0;
 
	setitimer(ITIMER_PROF,&new_value3,NULL);
 
 
	while(1);
	return 0;
}

信号发送函数—setitimer()

函数原型
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
参数说明
1.which:逻辑定时器类型
2.ITIMER_REAL:按实际时间(wall time)计时,计时到达时向进程发送SIGALRM信号。
3.ITIMER_VIRTUAL:计算进程在用户态执行的时间,计时到达将发送SIGVTALRM信号给进程。
4.ITIMER_PROF:计算进程在用户态和核心态的总时间(CPU time)。计时到达将发送SIGPROF信号给进程。
与ITIMER_VIRTUAL是一对,该定时器经常用来统计进程在用户态和核心态花费的时间
5.value:指明定时器的时间
6.ovalue:如果不为空,则保存上次调用设定的值
返回值
成功时,返回0;错误时,返回-1
struct itimerval结构定义
struct itimerval {
struct timeval it_interval; /定时器周期/
struct timeval it_value; /定时器时间/
};

struct timeval结构定义
struct timeval {
long tv_sec; //
long tv_usec; /微秒,1秒=1000000微秒/
};

工作机制
先对it_value倒计时,当it_value为零时触发信号,然后每隔it_interval,触发一次信号,一直这样循环下去。
it_value设置为0是不会触发信号的,所以要能触发信号,it_value得大于0;
如果it_interval为零,只会触发一次信号。


  • 信号实验三

  • 编写程序实现以下功能:
  • 进程A向进程B发送SIGUSR1信号;
  • 进程B收到信号后,打印字符串“receive SIGUSR1”;
  • 要求用sigqueue函数和sigaction函数实现以上功能;

这里可以再回头看一下kill、sigqueue,sigaction的区别。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handler(int sig,siginfo_t* p,void* q)
{
	if(sig == SIGUSR1)
	  printf("Received SIGUSR1!\n");
}
 
int main()
{
	union sigval mysigval;
	struct sigaction act;
 
	int pid;
	pid = fork();
 
	if(pid < 0)
		perror("fork");
	else if(pid == 0)
	{
		printf("This is the received process!\n");
        act.sa_sigaction = handler;
        act.sa_flags = SA_SIGINFO;
 
		if(sigaction(SIGUSR1,&act,NULL) < 0)
			perror("sigaction");
 
		while(1);
	}
	else
	{
		printf("This is the send process!\n");
		sleep(1);
		if(sigqueue(pid,SIGUSR1,mysigval) < 0)
		  perror("sigqueue");
	}
	return 0;
}

信号安装函数—sigaction()

原型定义
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction函数用于设定进程接收到特定信号后的行为。
第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。
第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,如果为空,进程会以缺省方式对信号处理;
第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
第二个参数最为重要,其中包含了对指定信号的处理函数、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。
底层系统调用

sys_sigaction(int sig, const struct old_sigaction __user *act, struct old_sigaction __user *oact)


sigaction结构定义如下

struct sigaction {
union{
__sighandler_t _sa_handler(int );
void ( * sa_sigaction)( int, siginfo_t*, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);//该属性暂且无用
}
由_sa_handler 指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;
由_sa_sigaction 指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t 结构的指针,结构中包含信号携带的数据值


siginfo_t的结构体

siginfo_t {
int si_signo; /* 信号值*/
int si_errno; /* errno值,错误代码*/
int si_code; /* 信号产生的原因*/
pid_t si_pid; /* 发送信号的进程ID,对实时信号有 意义 /
uid_t si_uid; /
发送信号进程的真实用户ID*/
int si_status; /* 退出状态*/
clock_t si_utime; /* 用户消耗的时间*/
clock_t si_stime; /* 内核消耗的时间*/
sigval_t si_value;
int si_int; //直接与sigval.sival_int关联
void* si_ptr; //直接与sigval.sival_ptr关联
void * si_addr; /* 触发fault的内存地址*/
int si_band;
int si_fd;
}


信号发送函数—sigqueue()

#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int signo, const union sigval sigval_t)
调用成功返回 0;否则,返回 -1。
功能
主要针对实时信号,支持带有参数信号,与函数sigaction()配合使用
参数说明
pid:接收信号的进程ID
signo:待发送信号
sigval_t:信号传递的参数(4字节)

说明
调用sigqueue()时,sigval_t被拷贝到信号处理函数
sigqueue()发送非实时信号时
sigval_t包含的信息仍然能够传递给信号处理函数
仍然不支持排队,所有相同信号都被合并为一个信号

sigqueue()的第三个参数是sigval联合数据结构
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
调用sigqueue()时,该数据结构中的数据将被拷贝到信号处理函数sigaction()的第二个参数
这样,就可在发送信号的同时让信号传递一些附加信息


  • 信号实验四

  • 编写程序实现以下功能:进程A向进程B发送信号;
  • 进程B收到进程A发送的信号后,打印出发送信号进程的pid,uid以及信号值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void handler(int sig,siginfo_t* act,void *p)
{
	printf("The sender's pid is: %d\n",act->si_pid);
	printf("The sender's uid is: %d\n",act->si_uid);
	printf("The sig is: %d\n",sig);
}
 
int main()
{
	int pid;
	union sigval mysigval;
	pid = fork();
	if(pid < 0)
	  perror("fork");
	else if(pid == 0)
	{
		printf("This is the received process!\n");
		struct sigaction act;
		act.sa_sigaction = handler;
		act.sa_flags = SA_SIGINFO;
		if(sigaction(SIGUSR1,&act,NULL) < 0)
		  perror("sigaction");
 
		while(1);
	}
	else
	{
		printf("This is the send process!\n");
		sleep(1);
		printf("The sender's pid is: %d\n",getpid());
		if(sigqueue(pid,SIGUSR1,mysigval) < 0)
		  perror("sigqueue");
	}
	return 0;
}

  • 信号实验五

  • 编写程序实现以下功能:
  • 进程A向进程B发送信号,该信号的附带信息为一个值为20的整数;
  • 进程B完成接收信号的功能,并且打印出信号名称以及随着信号一起发送过来的整形变量值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig,siginfo_t* info,void *p)
{
	printf("The num is: %d\n",info->si_value.sival_int);
}
 
int main()
{
	int pid;
	struct sigaction act;
	act.sa_sigaction = handler;
	act.sa_flags = SA_SIGINFO;
 
	pid = fork();
	if(pid < 0)
	  perror("fork");
	else if(pid == 0)
	{
		printf("This is the receive process!\n");
		if(sigaction(SIGUSR1,&act,NULL) < 0)
		  perror("sigaction");
 
		while(1);
	}
	else
	{
		printf("This is the send process!\n");
		union sigval mysigval;
		mysigval.sival_int = 20;
		
		sleep(1);
		
		if(sigqueue(pid,SIGUSR1,mysigval) < 0)
			perror("sigqueue");
	}
	return 0;
}

  • 信号实验六

  • 编写程序实现以下功能:
  • 进程A向进程B发送信号,该信号的附带信息为一个字符串“Hello world”;
  • 进程B完成接收信号的功能,并且打印出信号名称以及随着信号一起发送过来的字符串值。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig,siginfo_t* info,void *p)
{
	printf("The str is: %s\n",info->si_value.sival_ptr);
}
 
int main()
{
	int pid;
	struct sigaction act;
	act.sa_sigaction = handler;
	act.sa_flags = SA_SIGINFO;
 
	pid = fork();
	if(pid < 0)
	  perror("fork");
	else if(pid == 0)
	{
		printf("This is the receive process!\n");
		if(sigaction(SIGUSR1,&act,NULL) < 0)
		  perror("sigaction");
 
		while(1);
	}
	else
	{
		printf("This is the send process!\n");
		union sigval mysigval;
		mysigval.sival_ptr = "hello world";
		
		sleep(1);
		
		if(sigqueue(pid,SIGUSR1,mysigval) < 0)
			perror("sigqueue");
	}
	return 0;
}

  • 消息队列实验

  • 1.进程A向消息队列发送消息“hello,world”
  • 2.进程B从消息队列读取消息,并打印。
  • 3.进程C向消息队列发送“自己在姓名”
  • 4.进程D从消息队列中取出姓名字符串,并打印

msg结构体就相当于一个缓冲区,当创建一个消息队列后,不同的进程向消息队列发送消息,但是怎样读取特定的进程所发送的消息呢?在发送进程中标记自己所发消息的类型,在读取消息的进程中读取函数声明要读取消息的类型,这样就可以读取到特定的消息。
send进程:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msg{
        long msg_types;
        char msg_buf[512];
};
int main()
{
        int qid;
        int pid;
        int len;
	  int id;
     if((qid = msgget(IPC_PRIVATE, IPC_CREAT | 0666))<0)
        {
                perror("msgget");
                exit(1);
        }   struct msg pmsg;

id=fork();
if(id>0)
{
 pmsg.msg_types = getpid();
printf("%d",getpid());
        sprintf(pmsg.msg_buf, "hello,world");
        len = strlen(pmsg.msg_buf);

        if((msgsnd(qid, &pmsg, len, 0))<0)
        {
                perror("msgsnd");
			printf("%d",pid);
                exit(1);
        }
        printf("successfully send a message to the queue:%d\n",qid);
system("ipcs  -q");
exit(0);
}
else
{
sleep(1);
	  pmsg.msg_types = getpid();
		printf("%d",getpid());
        sprintf(pmsg.msg_buf, "自己在姓名");
        len = strlen(pmsg.msg_buf);
        if((msgsnd(qid, &pmsg, len, 0))<0)
        {
                perror("msgsnd");
			printf("%d",pid);
                exit(1);
        }
        printf("successfully send a message to the queue:%d\n",qid);
system("ipcs  -q");
exit(0);
}
       
        return 0;
}




receive进程:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define BUFSIZE 4096
struct msg{
        long msg_types;
        char msg_buf[511];
};
int main(int argc, char* argv[])
{
        int qid,len,id;
        struct msg pmsg;
        if(argc<2){
                printf("parameters error < 2\n");
                exit(1);
        }
        
id=fork();


if(id>0)
{

qid = atoi(argv[1]);
pmsg.msg_types = getpid();
 len = msgrcv(qid,&pmsg,BUFSIZE, 0, 1);
if(len > 0){
pmsg.msg_buf[len] = '\0';
printf("recving que id:%ld\n",qid);
printf("message type:%d\n",pmsg.msg_types);
printf("message length:%d\n",len);
printf("message text:%s\n",pmsg.msg_buf);
}else if(len == 0)
printf("no message!");
else{
perror("msgrcv");
exit(1);
       }
system("ipcs -q");
}


else
{
sleep(1);
qid = atoi(argv[1]);
pmsg.msg_types = getpid();
len = msgrcv(qid,&pmsg,BUFSIZE, 0, 1);
if(len > 0){
pmsg.msg_buf[len] = '\0';
printf("recving que id:%ld\n",qid);
printf("message type:%d\n",pmsg.msg_types);
printf("message length:%d\n",len);
printf("message text:%s\n",pmsg.msg_buf);
}else if(len == 0)
printf("no message!");
else{
perror("msgrcv");
exit(1);
       }
system("ipcs -q");
exit(0);

}
}


操作消息队列

1、 打开或创建消息队列.
2、 读写操作:消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构:
struct msgbuf{
long mtype;
char mtext[1];
};
mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。
对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;
对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可
3、 获得或设置消息队列属性:
消息队列API共有四个,使用时需要包括几个头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>


消息队列的基本操作—msgget()

功能
创建一个新消息队列或打开一个存在的队列
函数原型
int msgget(key_t key, int flag);
参数说明
key:待创建/打开队列的键值,如果key值为IPC_PRIVATE则创建一个新的消息队列。
flag:创建/打开方式
IPC_CREAT:如果存在与当前key值相同的消息队列,则返回该消息队列id。如果不存在,则创建一个新的消息队列。
IPC_EXCL:如果存在与当前key值相同的消息队列,则返回失败。
返回值
成功返回消息队列描述符,否则返回-1
说明
IPC_CREAT一般由服务器程序创建消息队列时使用
若是客户程序,须打开现有消息队列,而不用IPC_CREAT


发送消息—msgsnd()

函数原型
int msgsnd(int msqid, st**ruct msgbuf *msgp, size_t msgsize, int flag);
功能
向msqid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。
参数说明
对发送消息来说,有意义的flag标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。
造成msgsnd()等待的条件:
当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;
msgsnd()解除阻塞的条件有三个:
消息队列中有容纳该消息的空间;
msqid代表的消息队列被删除;
调用msgsnd()的进程被信号中断;
调用返回:
成功返回0,否则返回-1。


接收消息—msgrcv()

函数原型
ssize_t msgrcv(int msqid, struct msgbuf *msgp, size_t size, long type, int flag);
功能
该系统调用从msqid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。
参数说明
msqid:消息队列描述字,描述从哪个消息队列读取消息
msgp:消息存储位置
size:消息内容的长度
type:请求读取的消息类型
根据type的不同分成三种情况处理

type=0:接收该队列的第一个消息,并将它返回给调用者
type>0:接收类型type的第一个消息
type<0:接收小于等于type绝对值的最低类型的第一个消息


接收消息—msgrcv()工作流程
flag:规定队列无消息时msgrcv应做的操作
IPC_NOWAIT:如果现在没有消息,调用进程立即返回,同时返回-1。
IPC_EXCEPT:type>0时使用,返回第一个类型不为type的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将丢失。如果没有设置IPC_NOERROR,而消息又太长,则出错返回E2BIG,此时消息仍留在队列中。
调用返回:
成功返回读出消息的实际字节数,否则返回-1。
注意:
取消息的时候并不一定按照先进先出的次序取消息,可以按照消息的类型字段取消息。


  • 实验七:共享内存实验

  • 编写代码完成以下功能:
  • 1.创建共享内存,写进程通过键盘不断向内存写入“hello world”;
    如果结束写操作,则通过键盘输入“end”;
  • 2.读进程从共享内存读取数据,并打印。直到读到“end”为止。

共享内存就是一个公共的区域,获取该共享内存的id后就可读取和写入,通过地址映射的方式来进行操作,用地址来联系起来,会方便理解。


写入内容的进程:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define MAXSIZE 1024
struct shm{
	int write;        //记录读进程是否已经将内容读取
	char buffer[MAXSIZE];
};
 
int main()
{
	int shmid;
	void *shmptr = NULL;
	char str[MAXSIZE];    //存储输入的内容
	struct shm *share;
 
	if((shmid = shmget(0X44,MAXSIZE,0666|IPC_CREAT)) < 0)
		perror("shmget");
	if((shmptr = shmat(shmid,0,0)) == (void *)-1)
		perror("shmat");
 
	printf("This is the write process!!!\n");
	share = (struct shm *)shmptr;
	while(1)
	{
		if(share->write == 1)
		{
			sleep(1);
			printf("Waiting the read process!!!\n");
		}
 
		printf("please input hello world!!!\n");
		fgets(str,MAXSIZE,stdin);
		sprintf(share->buffer,"%s",str);
		share->write = 1;		
 
		if(strncmp(str,"end",3) == 0)
			break;
		sleep(1);
	}
	if(shmdt(shmptr) < 0)
		perror("shmdt");
	exit(0);
}

读走内容的进程

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <string.h>
#define MAXSIZE 1024
 
struct shm{
	int write;    //记录读进程是否已经将内容读取
	char buffer[MAXSIZE];
};
 
int main()
{
	int shmid;
	struct shm *share;
	void *shmptr = NULL;
	if((shmid = shmget(0X44,MAXSIZE,0666|IPC_CREAT)) < 0)
		perror("shmget");
	if((shmptr = shmat(shmid,0,0)) == (void *)-1)
		perror("shmat");
 
	printf("This is the read process!!!\n");
	share = (struct shm *)shmptr;
	while(1)
	{
		if(share->write != 0)
		{
			if(!strncmp(share->buffer,"end",3) == 0)
			{
				printf("%s",share->buffer);
				share->write = 0;
			}
			else{
					memset(share->buffer,0,sizeof(share->buffer));
					//sprintf(share->buffer,"%s"," ");
					break;

				}
				
		}

	}
 
	if(shmdt(shmptr) < 0)
		perror("shmdt");
 
	exit(0);
}

共享内存基本操作—shmget()

功能
创建/获得一个共享主存区
函数原型
int shmget(key_t key, int size, int flag);
参数说明
key:共享主存区键值.当key的取值为IPC_PRIVATE,则函数shmget()将创建一块新的共享内存;如果key的取值为0,而参数flag中设置了IPC_CREAT这个标志,则同样将创建一块新的共享内存。
size:共享内存大小(以字节计)
创建新共享内存:必须指定size
访问现存共享内存:size=0
Flag:函数的行为参数,指定当函数遇到阻塞或其他情况时应做出的反应。
返回值
成功返回创建的共享内存的标示符
失败则返回-1


共享主存基本操作—shmat()

功能
将共享主存区映射到进程虚拟地址空间
函数原型
void *shmat(int shmid, char *addr, int flag);
参数说明
shmid:需要映射的共享内存标识符;
addr:
=NULL:表示映射地址由内核自动选择(推荐方法) ;
!=NULL:映射地址取决于flag的值;
flag!=SHM_RND,映射地址等于addr指定的地址;
指定SHM_RND,进位到最低可用地址;
flag:
SHM_RND:表示第二个参数指定的地址应被向下靠拢到内存页面大小的整数倍
SHM_RDONLY:以只读方式映射到进程的地址空间;
SHM_REMAP:替代掉与指定参数重叠的现存共享区段的映射;
返回值
成功时返回映射区起始地址;
出错时为-1;


共享主存基本操作—shmdt()

功能
把一个共享主存区从指定进程的虚地址空间断开,如果当释放这个内存块的进程是最后一个使用该内存块的进程,则这个内存块将被删除。
函数原型
int shmdt(char *addr);
参数说明
addr:需断开连接的虚地址,即由shmat( )返回的虚地址。
返回值
调用成功返回0
失败时返回-1


  • 信号量实验

  • 编写程序完成以下功能:
  • 1.创建含有2个信号量的集合,第一个信号量的初始值是1,第二个信号量的初始值是3,对第一个信号量做加法操作,对第二个信号量做减法操作。
  • 2.在另外一个进程中判断信号量集合中两个信号量的值,当两个信号量在值相等在时候,打印“hello”

第一个进程:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
int main()
{
	key_t key;
	key = ftok(".",3);
 
	int sem_id;
	int sems = 2;
 
	sem_id = semget(key,sems,IPC_CREAT);
	if(sem_id < 0)
	  perror("semget");
 
	typedef union semnu{
		int value;
		struct semid_ds *buf;
		unsigned short int *array;
		struct seminfo *__buf;
	}sem;
 
	sem sem_0, sem_1;
	sem_0.value = 1;
        sem_1.value = 3;
	
	semctl( sem_id, 0, SETVAL, sem_0 );
	semctl( sem_id, 1, SETVAL, sem_1 );
 
	struct sembuf buf_p;
        buf_p.sem_num = 0;
        buf_p.sem_op = 1;
        buf_p.sem_flg = (IPC_NOWAIT | SEM_UNDO);
        int semop_value;
        semop_value = semop(sem_id, &buf_p, 1);
        if( semop_value<0 )
        {
            perror("semop(p) error:");
            exit(1);
        }
	
	struct sembuf buf_v;
        buf_v.sem_num = 1;
        buf_v.sem_op = -1;
        buf_v.sem_flg = (IPC_NOWAIT|SEM_UNDO);
        semop_value = semop(sem_id, &buf_v, 1);
        if( semop_value<0 )
        {
            perror("semop(v) error:");
            exit(1);
	}
 
	int sem0_val,sem1_val;
	sem0_val = semctl(sem_id,0,GETVAL);
	sem1_val = semctl(sem_id,1,GETVAL);
 
	printf("The sem0_val is: %d\n",sem0_val);
	printf("The sem1_val is: %d\n",sem1_val);
 
	return 0;
}

第二个进程:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
int main()
{
	key_t key;
	key = ftok(".",3);
 
	int sem_id;
	int sems = 2;
 
	sem_id = semget(key,sems,IPC_CREAT);
	if(sem_id < 0)
	  perror("semget");
 
	typedef union semnu{
		int value;
		struct semid_ds *buf;
		unsigned short int *array;
		struct seminfo *__buf;
	}sem;
 
	int sem0_val,sem1_val;
	sem0_val = semctl(sem_id,0,GETVAL);
	sem1_val = semctl(sem_id,1,GETVAL);
	
	if(sem0_val == sem1_val)
	  printf("hello\n");
 
	return 0;
}

信号量的基本操作—semget()

功能
创建一个新信号量集或取得一个已有信号量集的标示符
函数原型
int semget(key_t key, int nsems, int flag)
参数说明
nsems:需要创建的信号量数目,
最大值定义:#define SEMMSL 250[Linux/sem.h],若打开一个现有信号量集合,则nsems通常设置为0;
Flag:可以取IPC_CREAT,表示创建一个新的唯一的信号量。如果取IPC_EXCL | IPC_CREAT,表示创建一个新的信号量,如果信号量已存在,则返回一个错误。
返回值
调用成功返回信号量集描述符
失败时返回-1


信号量的基本操作—semctl()

功能:
对信号量进行控制,可用于删除一个信号量 。
函数原型
int semctl(int semid, int semnum, int cmd, union semun arg);
参数说明
semid由semget返回的信号量集标示符;
semnum:信号量集semid中的某一个信号量的索引;
cmd:对编号为semnum的信号量执行的操作命令;
arg:用于设置或返回信号量信息。(根据第三个参数而改变)
返回值:
调用成功返回0;失败时返回-1
semun结构体
union semun{
int val; // for SETVAL
struct semid_ds *buf; //for IPC_STAT and IPC_SET
unsigned short *array; //for GETALL and SETALL
}
cmd命令类型
IPC_STAT:将信号量集的信息复制到arg 参数,此时第二个参数无用;
IPC_SET:与上一个操作相反
IPC_RMID:删除信号量,不使用第四个参数
GETALL/SETALL:获取/设置所有信号量的值
GETVAL/SETVAL:获得/设置指定信号量的值

GETNCNT:返回等待semnum所代表信号量的值增加的进程数
GETPID:返回最后一个对semnum所代表信号量执行semop操作的进程ID
GETZCNT:返回等待semnum所代表信号量的值变成0的进程数


IPC键的创建

创建函数
key_t ftok( char * filename, int id);
功能说明
将一个现存文件名(对应文件必须是可访问的)和一个整数标识符id转换成一个key_t值
在Linux系统中,调用该函数时,系统将该文件的索引节点号取出,并在前面加上子序号,从而得到key_t返回值



实验5 线程实验

  • 线程练习一

  • 编程实现以下功能:
  • 主线程实现以下功能:
    定义全局变量key;
    创建两个线程;
    如果线程正常结束,得到线程的结束状态值,并打印;
  • 线程一完成以下操作:
    设置全局变量key的值为字符串“hello world”;
    打印字符串“当前线程ID:key值”;
    接收到线程二发送的取消请求信号后退出;
    结束的时候打印字符串“thread1 ,exited!:key值”;
  • 线程二完成以下操作:
    设置key值为6;
    给线程一发送取消请求信号;
    结束的时候打印字符串“thread2,exited!:key值”;
    带有线程的程序编译时需要加-lpthread,不然编译不会通过。
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
pthread_key_t key;
 
void cleanup(void *arg)
{
	printf("%s\n",(char *)arg);
}
 
void *child_thread1(void *arg)
{
	char *str = "Hello World";
	printf("The child_thread1 run!\n");
	printf("The thread id is: %d\n",syscall(SYS_gettid));/*打印该线程的id*/
	if(pthread_setspecific(key,str) < 0)/*设置key值为hello world*/
	  perror("pthread_setspecific");
 
	char *get_key = (char *)pthread_getspecific(key);/*获取key值地址并存入get_key中*/
	printf("The thread1's key is: %s\n",get_key);
 
	pthread_cleanup_push(cleanup,"Thread1,exited!");/*调用清理函数入栈*/
	pthread_cleanup_pop(1);/*从栈弹出一个*/
}
 
void *child_thread2(void *arg)
{
	int num = 6;
	printf("The child_thread2 run!\n");
 
	if(pthread_cancel((pthread_t)arg) < 0)/*向线程一发送取消请求*/
	  perror("pthread_cancle");
 
	if(pthread_setspecific(key,(void *)num) < 0)/*设置key值为6*/
	  perror("pthread_setspecific");
	int *get_key = (int *)pthread_getspecific(key);/*获取key值地址存入get_key中*/
 
	printf("The thread2's key is: %d\n",get_key);
	pthread_cleanup_push(cleanup,"Thread2,exited!");/*调用清理函数入栈*/
    pthread_cleanup_pop(1);/*从栈中弹出一个*/
}
 
void *thread(void *arg)
{
	pthread_t tid1,tid2;
	void *tret1,*tret2;
 
	printf("This is the main pthread!\n");
 
	if(pthread_key_create(&key,NULL) < 0)
	  perror("phtread_key_create");
	if(pthread_create(&tid1,NULL,(void *)child_thread1,NULL) < 0)/*创建线程一*/
	  perror("pthread_create");
	
	pthread_join(tid1,&tret1);/*等待线程一退出*/
	printf("The pthread1 exited is: %d\n",(long)tret1);/*打印线程一的退出状态*/
 
	if(pthread_create(&tid2,NULL,(void *)child_thread2,&tid1) < 0)/*创建线程二*/
	  perror("pthread_create");
 
	pthread_join(tid2,&tret2);/*等待线程二退出*/
	printf("The pthread2 exited is: %d\n",(long)tret2);/*打印线程二的退出状态*/
}
 
int main()
{
	pthread_t id;
	if(pthread_create(&id,NULL,(void *)thread,NULL) < 0)
	  perror("pthread_create");
 
	sleep(1);
	return 0;
}

线程创建

函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void * ),void * arg);
参数说明
thread: 要创建的线程id指针
attr: 创建线程时的线程属性
( *start_routine)(void * ):返回值是(void *)类型的指针函数
arg:start_routine的参数
返回值
成功返回0
失败返回错误编号
EAGAIN:表示系统限制创建新的线程,如线程数目过多
EINVAL:代表线程属性值非法


等待线程终止

函数原型
int pthread_join( pthread_t thread, void **rval_ptr);
功能
调用者将阻塞并等待新线程终止
当新线程调用pthread_exit()退出或者return时,进程中的其他线程可通过pthread_join()获得线程的退出状态

使用约束
一个新线程仅仅允许一个线程使用该函数等待它终止
被等待线程应处于可join状态,即非DETACHED状态
返回值
成功结束返回值为0,否则为错误编码
说明
类似于waitpid()


取消请求–发送线程

函数原形:
int pthread_cancel(pthread_t tid);
参数:
tid 线程 id
返回值:
若成功返回 0,否则返回错误编号。
功能:
取消同一进程中的其他线程
头文件:
#include <pthread.h>
当调用pthread_cancel函数后,向目标线程发取消信号,但如何处理取消信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点)。


取消请求–接收线程

设置线程的取消状态,即线程是否接受取消信号。
int pthread_setcancelstate(int state, int *oldstate)
,state有两种值:PTHREAD_CANCEL_ENABLE(接收取消信号)和PTHREAD_CANCEL_DISABLE(忽略取消信号,但取消信号还在),old_state如果不为NULL则存入原来的Cancel状态以便恢复。
设置取消类型,即接收取消请求的线程是立即终止还是在取消点终止。
int pthread_setcanceltype(int type, int *oldtype)
,type由两种取值:PTHREAD_CANCEL_DEFFERED(推迟至取消点执行取消请求)和PTHREAD_CANCEL_ASYCHRONOUS(立即执行取消请求),仅当Cancel状态为Enable时有效;oldtype如果不为NULL则存入原来的取消动作类型值。
取消点
1.根 据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、 pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系 统调用都是取消点。
2.通过函数void pthread_testcancel(void)指定取消点。可以在一个没有包含取消点的执行代码线程中通过调用pthread_testcancel函数设置一个取消点,响应取消请求.线程取消功能处于启用状态且取消状态设置为延迟状态时,pthread_testcancel()函数有效。
如果在取消功能处处于禁用状态下调用pthread_testcancel(),则该函数不起作用。


线程清理处理程序

函数原形:
void pthread_cleanup_push(void (*rtn)(void ),voidarg);
void pthread_cleanup_pop(int execute);
参数:
rtn 处理程序入口地址,arg 传递给处理函数的参数
功能:
线程清理处理程序
头文件:
#include <pthread.h>
返回值:

线程可以建立多个清理处理程序,处理程序记录在栈中,也就是说它 们的执行顺序与它们的注册顺序相反。
当线程执行以下动作时调用清理处理程序:
1.调用pthread_exit时;
2.响应取消请求时;
3.pthread_cleanup_pop函数中的参数不为0。
执行以下动作时,不会调用清理处理程序
pthread_cleanup_pop函数中的参数为0 ,清理函数不被调用。该参数不影响异常终止时,清理程序的执行。
注意:pthread_cleanup_push和pthread_cleanup_pop必须成对出现。


线程私有数据(Thread-specific Data)

在多线程程序中,所有线程共享程序中的全局变量。而如果每个线程希望能单独拥有一个全局变量,那么就需要使用线程私有数据。
表面上看起来这是一个全局变量,所有线程都可以使用它,而它的值在每一个线程中又是单独存储的。这就是线程私有数据的意义。


相关数据类型:
全局变量的类型pthread_key_t
相关操作:
创建线程私有数据:
int pthread_key_create(pthread_key_t *key, void ( * destructor)(void * ));
第二个参数是一个清理函数,在释放key的时候被调用,通常设为NULL,表示系统提供一个默认的清理函数。
读写线程私有数据
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);
注销线程私有数据
int pthread_key_delete(pthread_key_t key);


  • 互斥锁实验

  • 2.创建两个线程,使用互斥锁来实现对变量 lock_var 的加一、打印操作

互斥锁对于公共的内存空间的访问进行互斥化,当拥有锁的时候才可以操作,只有被解锁以后其他人才需要再次加锁才能进行操作,当操作完成以后需要解锁。如果上了两次锁就需要解锁两次,不然会变成死锁。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
pthread_mutex_t mutex;
void *thread_add();
void *thread_print();
int lock_var; //共享内存空间,需要互斥访问
int  main(int argc, char* argv[]) {
	pthread_t thread1_id,thread2_id;
	pthread_mutex_init( &mutex, NULL );
	lock_var=0;
	printf("init lock_var = 0 \n");
	pthread_create(&thread1_id, NULL, thread_add, NULL);
	pthread_create(&thread2_id, NULL, thread_print, NULL);
	//pthread_mutex_lock(&mutex);
	pthread_join(thread1_id, NULL);
	pthread_join(thread2_id, NULL);
	pthread_mutex_unlock(&mutex);
	pthread_mutex_destroy(&mutex);
	return 0;
}
void *thread_add() {
	printf("thread_add()\n");
	pthread_mutex_lock(&mutex);
	lock_var=lock_var+1;
	pthread_mutex_unlock(&mutex);
	sleep(1);
}

void *thread_print() {
	sleep(1);
	printf("thread_print()\n");
	pthread_mutex_lock(&mutex);
	printf("lock_var: %d\n",lock_var);
	pthread_mutex_unlock(&mutex);
}


互斥量

互斥量用 pthread_mutex_t 数据类型来表示,
互斥锁有两种初始化方式:
静态初始化 : 直接给其赋值为PTHREAD_MUTEX_INITIALIZER
动态初始化 : 通过调用 pthread_mutex_init 函数进行初始化,如果动态地分配互斥量,那么释放内存前需要调用 pthread_mutex_destroy.
互斥锁机制主要包括下面的基本函数。

  1. 互斥锁初始化:pthread_mutex_init()
  2. 互斥锁上锁:pthread_mutex_lock()
  3. 互斥锁判断上锁:pthread_mutex_trylock()
  4. 互斥锁解锁:pthread_mutex_unlock()
  5. 消除互斥锁:pthread_mutex_destroy()

初始化互斥锁

功能:
初始化互斥锁。
头文件:
#include <pthread.h>
函数原形:
int pthread_mutex_init(pthread_mutex_t * mutex, const pthread_mutex_t * attr);
参数:
mutex 互斥量
attr 互斥锁属性
返回值:
若成功则返回 0,否则返回错误编号。


销毁互斥锁

功能:
销毁互斥锁
头文件:
#include <pthread.h>
函数原形:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex 指向要销毁的互斥锁的指针。
返回值:
若成功则返回 0,否则返回错误编号。


对互斥量加减锁

功能:
对互斥量加/减锁
头文件:
#include <pthread.h>
函数原形:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数: pthread_mutex_t mutex的地址,即&mutex
返回值:
若成功则返回 0,否则返回错误编号。


  • 多线程生产者消费者

  • 用多线程实现生产者消费者;

一个生产者负责一个消费者,来进行循环。这里有两个生产者两个消费者。

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>

int number=0;

pthread_mutex_t lock;         //互斥锁                
pthread_cond_t noempty1;     //不空条件变量1
pthread_cond_t nofull1;     //不满条件变量1
pthread_cond_t noempty2;   //不空条件变量2
pthread_cond_t nofull2;   //不满条件变量2


void* profunc1()
{
    while(1)
    {
        pthread_mutex_lock(&lock);
        if(number>5)
        {
            printf("生产者1:仓库不能满足生产,等待消费!\n");
            pthread_cond_wait(&nofull1,&lock);
        }
     number++;
     printf("生产者1:生产1个红薯,现在仓库有%d个红薯\n",number);
     pthread_cond_signal(&noempty1);
     pthread_mutex_unlock(&lock);
     sleep(1);
    }
    return NULL;
}

void* profunc2()
{
     while(1)   
     {
         pthread_mutex_lock(&lock);
          if(number>4)
          {
              printf("生产者2:仓库不能满足生产,等待消费!\n");
              pthread_cond_wait(&nofull2,&lock);
          }
         number+=2;
         printf("生产者2:生产2个红薯,现在仓库有%d个红薯\n",number);
         pthread_cond_signal(&noempty2);
         pthread_mutex_unlock(&lock);
         sleep(1);
     }
    return NULL;
}

void* confunc1()
{
    while(1)
    {
        pthread_mutex_lock(&lock);
        if(number==0)
        {
            printf("消费者1:仓库已空,等待生产!\n");
            pthread_cond_wait(&noempty1,&lock);
        }
        number--;
        printf("消费者1:吃掉1个红薯,现在仓库还有个%d红薯\n",number);
        pthread_cond_signal(&nofull1);
        pthread_mutex_unlock(&lock);
        sleep(1);
    }
    return NULL;
}

 void* confunc2()
 {
      while(1)
      {
          pthread_mutex_lock(&lock);
          if(number<2)
          {
              printf("消费者2:仓库红薯少于2个,等待生产!\n");
              pthread_cond_wait(&noempty2,&lock);
          }
          number-=2;
         printf("消费者2:吃掉2个红薯,现在仓库还有个%d红薯\n",number);
          pthread_cond_signal(&nofull2);
          pthread_mutex_unlock(&lock);
          sleep(1);
      }
      return NULL;
  }

int main()
{
    pthread_t con1,con2,pro1,pro2;

    pthread_mutex_init(&lock,NULL);
    pthread_cond_init(&noempty1,NULL);
    pthread_cond_init(&nofull1,NULL);
    pthread_cond_init(&noempty2,NULL);
    pthread_cond_init(&nofull2,NULL);


    pthread_create(&pro1,NULL,profunc1,0);
    pthread_create(&pro2,NULL,profunc2,0);
    pthread_create(&con1,NULL,confunc1,0);
    pthread_create(&con2,NULL,confunc2,0);

    pthread_join(pro1,NULL);
    pthread_join(pro2,NULL);
    pthread_join(con1,NULL);
    pthread_join(con2,NULL);
    
    pthread_mutex_destroy(&lock);
    pthread_cond_destroy(&nofull1);
    pthread_cond_destroy(&noempty1);
    pthread_cond_destroy(&nofull2);
    pthread_cond_destroy(&noempty2);

    return 0;
}

初始化条件变量

使用条件变量之前要先进行初始化:
可以在单个语句中初始化一个条件变量如:
pthread_cond_t my_condition=PTHREAD_COND_INITIALIZER;
也可以利用函数 pthread_cond_init 动态初始化。
函数原形:
int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
参数:
cond:条件变量;attr :条件变量属性。
返回值:
成功返回 0,出错返回错误编号。
功能:
使用变量 attr 所指定的属性来初始化一个条件变量,如果参数 attr 为空, 那么它将使用缺省的属性来设置所指定的条件变量。


销毁条件变量

头文件:
#include < pthread.h>
函数原形:
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
cond指定需要销毁的条件变量
返回值:
成功返回 0,出错返回错误编号。
功能:
pthread_cond_destroy 函数可以用来摧毁所指定的条件变量,同时将会释放所给它分配的资源。


条件变量–等待操作

名称:
pthread_cond_wait/pthread_cond_timedwait
头文件:
#include < pthread.h>
函数原形:
int pthread_cond_wait(pthread_cond_t*cond, pthread_mutex_t mutex);
int pthread_cond_timedwait(pthread_cond_t
cond, pthread_mutex_t mutex, const struct timespec *abstime);
参数:
cond 条件变量,mutex 互斥锁
返回值:
成功返回 0,出错返回错误编号。
thread_cond_timedwait 和 pthread_cond_wait 一样,自动解锁互斥量及等待条件变量,但它还限定了等待时间。如果在 abstime 指定的时间内 cond 未触发,互斥量 mutex 被重新加锁,且 pthread_cond_timedwait 返回错误 ETIMEDOUT。abstime 参数指定一个绝对时间,时间原点与 time() 和 gettimeofday() 相同:abstime = 0 表示 1970 年 1 月 1 日 00:00:00 GMT。


phtread_cond_wait 的内部实现

(注意不是外部通常程序的加解锁)可以理解为如下几个步骤:
1 unlock mutex
2 sleep & wait for cond
3 cond ok & return
4 lock mutex


条件变量通知

函数原形:
int pthread_cond_signal(pthread_cond_t * cond);
int pthread_cond_broadcast(pthread_cond_t * cond);
参数:
cond 条件变量
返回值:
成功返回 0,出错返回错误编号。
当调用pthread_cond_signal 时,一个在相同条件变量上阻塞的线程将被解锁。如果同时有多个线程阻塞,则由调度策略确定接收通知的线程。如果调用pthread_cond_broadcast,则将通知阻塞在这个条件变量上的所有线程。一旦被唤醒线程仍然会要求互斥锁。 如果当前没有线程等待通知,则上面两种调用实际上成为一个空操作。如果参数*cond 指向非法地址,则返回值 EINVAL。


实验6 网络编程

  • TCP/UDP的编写实验

  • 1.完成基于Tcp的客户端和服务器程序编写;,要求服务器采用循环方式处理客户端的数据。
  • 2.完成基于UDP的客户端和服务器程序编写,要求服务器采用循环方式处理客户端的数据。

网络编程东西较多,但是用linux一切皆文件的思想来看就比较简单了。这里要特别注意这里的系统调用的参数,在我的linux机器中是我的地址,得理解这些参数的意义。


clientTCP进程:

#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 
#include <string.h> 
#include <netdb.h> 
#include <sys/types.h> 
#include <netinet/in.h> 
#include <sys/socket.h> 
#define SERVPORT 3333 
#define MAXDATASIZE 100 
//client 
main(int argc,char *argv[]){ 
	int sockfd,sendbytes; 
	char buf[MAXDATASIZE]; 
	char buf2[MAXDATASIZE]; 
	struct hostent *host; 
	struct sockaddr_in serv_addr; 
	if(argc < 2){ 
	fprintf(stderr,"Please enter the server'shostname!\n"); 
	exit(1); 
	} 
	/*地址解析函数*/ 
	if((host=gethostbyname(argv[1]))==NULL){ 
	perror("gethostbyname"); 
	exit(1); 
	} 
	/*创建 socket*/ 
	if((sockfd=socket(AF_INET,SOCK_STREAM,0))== -1){ 
	perror("socket"); 
	exit(1); 
	} 
	/*设置 sockaddr_in  结构体中相关参数*/ 
	serv_addr.sin_family=AF_INET; 
	serv_addr.sin_port=htons(SERVPORT); 
	serv_addr.sin_addr=*((struct in_addr *)host->h_addr); 
	bzero(&(serv_addr.sin_zero),8); 
	/*调用 connect 函数主动发起对服务器端的连接*/ 
	
	if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))== -1){ 
	perror("connect"); 
	exit(1); 
	} 
	while(1){
		/*发送消息给服务器端*/ 
		printf("请输入字符:");
		scanf("%s",buf2);
		printf("%d",strlen(buf2));
		if((sendbytes=write(sockfd,buf2,strlen(buf2)))== -1){ 
		perror("send"); 
		exit(1); 
		}
		if(strcmp(buf2,"end")==0)break;
		memset(buf2,0,sizeof(buf2));
	}
	close(sockfd); 
} 

serverTCP进程:

#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 
#include <string.h> 
#include <unistd.h> 
#include <netinet/in.h> 
#define SERVPORT 3333 
#define BACKLOG 10 
#define MAX_CONNECTED_NO 10 
#define DEST_IP "192.168.126.130"
#define MAXDATASIZE 20
//server
int main() 
{ 
	struct sockaddr_in server_sockaddr,client_sockaddr; 
	int sin_size,recvbytes; 
	int sockfd,client_fd; 
	char buf[MAXDATASIZE]; 
	/*建立 socket 连接*/ 
	if ((sockfd=socket(AF_INET,SOCK_STREAM,0) )== -1){ 
	perror("socket"); 
	exit(1); 
	} 
	printf("socket success!,sockfd=%d\n",sockfd); 
	/*设置 sockaddr_in  结构体中相关参数*/ 
	server_sockaddr.sin_family=AF_INET; 
	server_sockaddr.sin_port=htons(SERVPORT); 
	server_sockaddr.sin_addr.s_addr=inet_addr(DEST_IP);; 
	bzero(&(server_sockaddr.sin_zero),8); 
	/*绑定函数 bind*/ 
	if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))== -1){ 
	perror("bind"); 
	exit(1); 
	} 
	printf("bind success!\n"); 
	/*调用 listen 函数*/ 
	if(listen(sockfd,BACKLOG)== -1){ 
	perror("listen"); 
	exit(1); 
	} 
	printf("listening....\n"); 
	/*调用 accept 函数,等待客户端的连接*/ 
	if((client_fd=accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)) == -1){ 
	perror("accept"); 
	exit(1); 
	} 

	while(1){
			/*调用 recv 函数接收客户端的请求*/ 
		if((recvbytes=read(client_fd,buf,MAXDATASIZE))== -1){ 
		perror("recv"); 
		exit(1); 
		} 
		printf("received a connection :%s\n",buf); 
		if(strcmp(buf,"end")==0)break;
		memset(buf,0,sizeof(buf));
	}
	
	close(sockfd); 
} 

clientUDP进程:

#include<sys/types.h>
#include<sys/socket.h>    
#include<linux/in.h>     
#include<string.h>    
#include<stdio.h>
//client 
void udpclie(int,struct sockaddr*);
int main(int argc,char *argv[])
{
	int s;
	struct sockaddr_in addr_ser;
	s = socket(AF_INET,SOCK_DGRAM,0);//SOCK_DGRAM UDP协议
	memset(&addr_ser,0,sizeof(addr_ser));//清空
	addr_ser.sin_family = AF_INET;//IPV4
	addr_ser.sin_addr.s_addr = htonl(INADDR_ANY);//表示从本机的任一网卡接收数据
	addr_ser.sin_port = htons(8888);//指定端口号
	udpclie(s,(struct sockaddr*)&addr_ser);
	close(s);
	return 0;
}
void udpclie(int s,struct sockaddr*to)
{
	char buff[256];
	struct sockaddr_in from;
	socklen_t len = sizeof(*to);
	while(1){
	printf("请输入要发给服务器的内容: "); 
	scanf("%s",buff); 
	sendto(s,buff,256,0,to,len);//发数据
	recvfrom(s,buff,sizeof(buff),0,(struct sockaddr*)&from,&len);
	printf("客户收到了: %s\n",buff);
	if(strcmp(buff,"end")==0)return ;
	}
}


serverUDP进程:

#include<sys/types.h>
#include<sys/socket.h> 
#include<linux/in.h>  
#include<string.h>   
#include<stdio.h>
#include<stdlib.h>
//server 
void udpserv_echo(int ,struct sockaddr*);
int main(int argc,char *argv[])
{
	int s;
	struct sockaddr_in addr_ser,addr_clie;
	s = socket(AF_INET,SOCK_DGRAM,0);// 生成套接字文件描述符
	memset(&addr_ser,0,sizeof(addr_ser));
	 // 通过struct sockaddr_in 结构设置服务器地址和监听端口
	addr_ser.sin_family = AF_INET;
	addr_ser.sin_addr.s_addr = htonl(INADDR_ANY);
	addr_ser.sin_port = htons(8888);//指定端口号 
	bind(s,(struct sockaddr*)&addr_ser,sizeof(addr_ser));
	udpserv_echo(s,(struct sockaddr*)&addr_clie);
	return 0;
}
void udpserv_echo(int s,struct sockaddr*addr_clie)
{
	char buff[256];
	int i=0;
	int n;
	char str[10];
	socklen_t len;
	while(1)
	{ 	
		len = sizeof(*addr_clie);
		n=recvfrom(s,buff,256,0,addr_clie,&len);//发数据
		printf("服务器收到了: %s\n",buff);
		sendto(s,buff,n+1,0,addr_clie,len);
		if(strcmp(buff,"end")==0)return ;
	}

}


传输层协议

传输层包括两个协议:
TCP协议:
即传输控制协议,是一个可靠的、面向连接的协议。
解 释 : 传输控制协议 TCP:
1.TCP是面向连接的协议。所谓连接,就是两个对等实体为进行数据通信而进行的一种结合。面向连接服务是在数据交换之前,必须先建立连接。当数据交换结束后,则应终止这个连接。
2.面向连接服务具有:连接建立、数据传输和连接释放这三个阶段。在传送数据时是按序传送的。
UDP协议:
采用无连接的方式,不管发送的数据包是否到达目的主机,数据包是否出错。收到数据包的主机也不会告诉发送方是否正确收到了数据,它的可靠性是由上层协议来保障的。
解 释 :用户数据报协议UDP:
1.UDP是无连接的服务。在无连接服务的情况下,两个实体之间的通信不需先建立好一个连接,因此其下层的有关资源不需要事先进行预定保留。这些资源将在数据传输时动态地进行分配。
2.无连接服务的另一特征就是它不需要通信的两个实体同时是活跃的(即处于激活态)。当发送端的实体正在进行发送时,它才必须是活跃的。优点是灵活方便和比较迅速,但不能防止报文的丢失、重复或失序,特别适合于传送少量零星的报文。


socket()函数

功能
创建一个套接字
头文件
#include <sys/socket.h>
函数原型
int socket(int domain, int type, int protocol);
参数说明
domain:通信协议族,即地址族
type:套接字类型
protocol:通信协议
常设置为0 ,由内核根据指定的类型和协议族使用默认的协议
返回值
成功时,返回一个大于等于0的文件描述符
失败时,返回一个小于0的值


connect()函数

功能
建立套接字连接
头文件
#include <sys/socket.h>
函数原型
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
参数说明
sockfd:调用socket返回的文件描述符
serv_addr:远程主机IP 地址和端口
addrlen:设置为 sizeof(struct sockaddr)
返回值
成功时,返回0
失败时,返回-1


sockaddr结构定义

功能
保存socket信息
结构
struct sockaddr {
unsigned short sa_family; /* 地址族,AF_xxx / char sa_data[14]; / 协议地址 */
};
说明
sa_family一般为AF_INET(表示TCP/IP)
sa_data包含socket的IP地址和端口号
/include/linux/socket.h


sockaddr_in结构体

功能
sockaddr的另一种表示形式
结构
struct sockaddr_in { short int sin_family; / * 地址族 * /
unsigned short int sin_port; / * 端口号 * /
struct in_addr sin_addr; / * IP地址 * /
unsigned char sin_zero[8]; / * 填充0,保持与struct sockaddr等长 * /
};
说明
sin_zero用于将sockaddr_in结构填充到与struct sockaddr等长,可用bzero( )或memset( )函数将其置为0
当sin_port = 0时,系统随机选择一个未被使用的端口号
当sin_addr = INADDR_ANY时,表示从本机的任一网卡接收数据。
指向sockaddr_in的指针和指向sockaddr的指针可相互转换


send()函数

功能
通过socket发送数据
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明
sockfd:发送数据的套接字描述符
buf:指向发送数据的指针
len:数据长度
flags:一般设置为0
返回值
成功时,返回实际发送的数据的字节数
失败时,返回-1


recv()函数

功能
通过socket接收数据
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明
sockfd:要读的SOCKET描述符。
buf:要读的信息的缓冲区。
len:缓冲的最大长度。
flags:一般设置为0。
返回值
成功时,返回实际接收到的数据的字节数。
失败时,返回-1。


bind()函数

功能
将套接字地址与所创建的套接字号联系起来
客户端如果只想使用connect(),则无须使用该函数
头文件
#include <sys/socket.h>
函数原型
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
参数说明
sockfd:调用socket返回的文件描述符
my_addr:保存地址信息(IP地址和端口)
addrlen:设置为 sizeof(struct sockaddr)
返回值
成功时,返回0
失败时,返回-1


listen()函数

功能
用于面向连接服务器,监听链接请求队列
头文件
#include <sys/socket.h>
函数原型
int listen(int sockfd, int backlog);
参数说明
sockfd:调用socket返回的文件描述符
backlog:accept()应答之前,允许在进入队列中等待的连接数目,出错时返回-1
返回值
成功时,返回0
失败时,返回-1
说明
在使用listen()之前,需要调用bind()绑定到需要的端口,否则系统内核将会监听一个随机端口


accept()函数

功能
建立套接字连接,处理单个连接请求(如发送/接收数据)
头文件
#include <sys/socket.h>
函数原型
int accept(int sockfd, struct void*addr, socklen_t *addrlen);
参数说明
sockfd:正在监听端口的套接字文件描述符
addr:指向本地数据结构sockaddr_in的指针
调用connect()的信息将存储在该结构中
addrlen:设置为sizeof(struct sockaddr_in)
返回值
成功时,返回一个socket 端口
失败时,返回-1


sendto()函数

功能
用于数据报套接字的通信
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);
参数说明
to:目地机的IP地址和端口号信息
tolen:常被赋值为sizeof (struct sockaddr)
返回值
成功时,返回实际发送的数据的字节数
失败时,返回-1


recvfrom()函数

功能
用于数据报套接字的通信
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int recvfrom(int sockfd,void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
参数说明
from:保存源机的IP地址及端口号
fromlen:常常被赋值为sizeof (struct sockaddr)
返回值
成功时,返回实际接收到的数据的字节数
失败时,返回-1



  • 12
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1.什么是Linux操作系统? Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。它能运行主要的UNIX工具软件、应用程序和网络协议。它支持32位和64位硬件。Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统。它主要用于基于Intel x86系列CPU的计算机上。这个系统是由全世界各地的成千上万的程序员设计和实现的。其目的是建立不受任何商品化软件的版权制约的、全世界都能自由使用的Unix兼容产品 !   Linux以它的高效性和灵活性著称。Linux模块化的设计结构,使得它既能在价格昂贵的工作站上运行,也能够在廉价的PC机上实现全部的Unix特性,具有多任务、多用户的能力。Linux是在GNU公共许可权限下免费获得的,是一个符合POSIX标准的操作系统。Linux操作系统软件包不仅包括完整的Linux操作系统,而且还包括了文本编辑器、高级语言编译器等应用软件。它还包括带有多个窗口管理器的X-Windows图形用户界面,如同我们使用Windows NT一样,允许我们使用窗口、图标和菜单对系统进行操作。   Linux具有Unix的优点:稳定、可靠、安全,有强大的网络功能。在相关软件的支持下,可实现WWW、FTP、DNS、DHCP、E-mail等服务,还可作为路由器使用,利用ipchains/iptables可构建NAT及功能全面的防火墙。 2.Linux的目录结构

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lazy_Goat

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值