函数原型以及部分实例应用
-------------------------------------------------------------------------------------------------------------------------------------
                                                             动态内存分配
-------------------------------------------------------------------------------------------------------------------------------------
(1)malloc函数
函数原型为:
void *malloc(unsigned int size);
该函数的功能是分配长度为size字节的内存块。
如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。注意当内存不再使用时,要使用free()函数释放内存块。例如使用
malloc函数获得一块内存空间,内存空间的大小与返回的指针类型由程序员根据需要自行规定,代码如下:
void main()
{
long* buffer;
buffer = (long *)malloc(400); //获得一块长整型数组空间
free(buffer); //释放内存空间
}
(2)calloc函数
函数原型为:
void *calloc(unsigned n,unsigned size);
该函数的功能是在内存的动态区存储中分配n个长度为size的内存块。
如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL,同样在内存不再使用时要用free()函数释放内存块。
同时,用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。
例如,使用calloc函数获得一块长整型数组空间,代码如下:
void main()
{
long* buffer;
buffer = (long *)calloc(20,sizeof(long)); //获得一块长整型数组空间
free(buffer); //释放内存空间
}
(3)realloc函数
函数原型为:
void *realloc(void *mem_address,unsigned int newsize);
该函数的功能是调整mem_address所指内存区域的大小为newsize长度。
如果重新分配内存成功,则返回指向被分配内存的指针,否则返回空指针NULL。并且当内存不再使用时,要应用free()函数将内存空间
释放。
当参数mem_address指向NULL时,即调整空指针所指向的内存区域的大小为newsize长度,此时realloc函数的功能就如同malloc
函数。若参数newsize为0,即要调整成的长度为0时,函数realloc所实现的功能相当于free函数,释放掉该内存区块。
例3.1 在vim编辑器中编写一个简单的C语言程序,使用realloc函数重新分配一块内存空间。
#include<stdlib.h>
#include<stdio.h>
main()
{
char *p;
p=(char *)malloc(100); /* 为指针p开辟一个内存空间*/
if(p) /*判断内存分配成功与否*/
printf("Memory Allocated at: %x",p);
else
printf("Not Enough Memory!\n");
getchar();
p=(char *)realloc(p,256); /*调整p内存空间从100字节调整到256字节*/
if(p)
printf("Memory Reallocated at: %x",p);
else
printf("Not Enough Memory!\n");
free(p); /*释放看p所指向的内存空间*/
getchar();
return 0;
}
(4)memset函数 (替换数组内容)
函数原型为:
void *memset(void *s,char ch,unsigned n);
该函数的功能是设置s中的所有字节为ch,s数组的大小为n。
例3.2 在vim编辑器中编写一个简单的C语言程序,演示memset函数的功能,使用字符‘*’替换数组s中的字符串。
#include<string.h>
#include<stdio.h>
int main(void)
{
char s[] = "welcome to mrsoft\n"; /*定义一个字符数组s*/
printf("s before memset: %s\n", s); /*输出字符数组中的内容*/
memset(s, '*', strlen(s) 1)
; /*设置s数组中的字符串内容为“*”*/
printf("s after memset: %s\n", s); /*输出此时的字符数组内容*/
return 0;
}
(5)free函数的原型为:
void free( void *memblock );
参数memblock表示要被释放的内存区块。
-------------------------------------------------------------------------------------------------------------------------------------
                                                                     进程创建
-------------------------------------------------------------------------------------------------------------------------------------
进程创建
进入进程的运行状态时,需要首先创建一个新进程,在Linux系统中,提供了几个关于创建新进程的操作函数,比如fork函数、vfork函
数和exec函数族等,下面分别对它们进行讲解。
fork函数
fork函数的功能是创建一个新的进程,新进程为当前进程的子进程,那么此当前的进程就被称之为父进程。在一个函数中,可以通过fork
函数的返回值,判断进程是在子进程中还是在父进程中。
fork函数的调用形式为:
pid_t fork(void);
使用fork函数需要引用<sys/types.h>和<unistd.h>头文件,该函数的返回值类型为pid_t类型,表示一个非负整数。若程序运行在父
进程中,函数返回的PID为子进程的进程号;若运行在子进程中,返回的PID为0。(自定义功能函数一般在子进程运行)
如若调用fork函数创建子进程失败,那么就会返回1
。并且提示错误信息,错误信息有以下两种形式:
EAGAIN:表示fork函数没有足够的内存用于复制父进程的分页表和进程结构数据。
ENOMEM:表示fork函数分配必要的内核数据结构时,内存不足。
下面通过一个实例,讲解如何使用fork函数,并通过该实例演示进程的创建过程。
在Linux系统中,使用vim编辑器编写如下代码,以便使用fork函数创建子进程。
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(void)
{
pid_t pid;
if((pid=fork())<0) /*创建新进程*/
{
printf("fork error!\n");
exit(1);
}
else if(pid==0) /*新创建的子进程*/
{
printf("in the child process!\n");
}
else
{
printf("in the parent process!\n");
}
exit(0);
}
调用fork函数
由程序的结果可以发现fork函数的一个特点,那就是“调用一次,返回两次”,这样的特点是如何出现的呢?
fork函数的特点
上图可以看出,在一个程序中,调用到fork函数后,就出现了分叉,在子进程中,fork函数返回0,在父进程中,fork函数返回子进
程的ID,因此,fork函数返回值后,开发人员可以根据返回值的不同,对父进程和子进程执行不同的代码,这样就使得fork函数具有“一
次调用,两次返回”的特点。
但是,父进程与子进程的返回顺序,并不是固定的,由于fork函数是系统调用函数,因此取决于系统中其他的进程的运行情况和内核的调
度算法。
vfork函数
vfork函数与fork函数相同,都是系统调用函数,两者的区别是在创建子进程时,fork函数会复制所有的父进程的资源,包括进程环境、
内存资源等。而vfork函数在创建子进程时,不会复制父进程的所有资源,父子进程共享地址空间,这样在子进程中对虚拟内存空间中变
量的修改,实际上是在修改父进程虚拟内存空间中的值。
注意:在使用vfork函数时,父进程会被阻塞,需要在子进程中调用_exit函数退出子进程,不能使用exit退出函数。
下面通过一个实例,演示vfork函数与fork函数的区别。
在Linux系统中,使用vim编辑器编写如下代码,调用vfork函数创建子进程,观察它与fork()的区别。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int gvar=2;
int main(void)
{
pid_t pid;
int var=5;
printf("process id:%ld\n",(long)getpid());
printf("gvar=%d var=%d\n",gvar,var);
if((pid=vfork())<0) /*创建一个新进程*/
{
perror("error!");
return 1;
}
else if(pid==0)
{ /*子进程*/
gvar--;
var++;
printf("the child process id:%ld\ngvar=%d var=%d\n",(long)getpid(),gvar,var);
_exit(0);
}
else
{ /*父进程*/
printf("the parent process id:%ld\ngvar=%d var=%d\n",(long)getpid(),gvar,var);
return 0;
}
}
本实例中,首先定义了一个全局变量和一个局部变量,在子进程中改变了全局变量与局部变量的值,并将其输出,并且在父进程中也输出
了这两个变量的值,由运行结果,可以看出,父进程中输出的变量值也是在子进程中变化后的值,由此可知,调用vfork函数,改变子进
程中的值,其实就是改变了父进程中的值。
vfork函数的运行结果
& 说明:读者可以将程序中的vfork函数改为调用fork函数,观察变量值的变化,可以更加清晰的了解两个函数在使用上的区别。
þ exec函数族
通过调用fork函数和vfork函数创建子进程,子进程和父进程执行的代码是相同的,但是,通常创建了一个新进程也就是子进程后,目的
是要执行与父进程不同的操作,实现不同的功能。因此,Linux系统提供了一个exec函数族,用于修改子进程。调用exec函数时,子进
程中的代码段、数据段和堆栈段都将被替换。由于调用exec函数并没有创建新进程,因此修改后的子进程的ID并没有改变。
exec函数族由6种以exec开头的函数组成,定义形式分别如下:
int execl(const char *path,const char *arg,…);
int execlp(const char *file,const char *arg,…);
int execle(const char *path,const char *arg,…,char* const envp[]);
int execv(const char *path,const char *argv[]);
int execve(const char *path,const char *argv[],char *const envp[]);
int execvp(const char *file,const char *argv[]);
这些函数都定义在系统函数库中,在使用前需要引用头文件<sys/types.h>和<unistd.h>,并且必须在预定义时定义一个外部的全局变
量,如:
extern char **environ;
上面定义的变量是一个指向Linux系统全局变量的指针,定义了这个变量后,就可以在当前工作目录中执行系统程序,如同在shell中不
输入路径直接运行vim和emacs等程序一样。
exec函数族中的函数都实现了对子进程中的数据段、代码段和堆栈段进行替换的功能,如果调用成功,则加载新的程序,没有返回值。
如果调用出错,则返回值为1

这几个exec函数,书写方式很相似,很容易记混,但是这几个函数又都各有区别。通过对exec函数名称的拼写规律可以轻松帮助读者牢
牢记住这几个函数实现替换功能的不同方法。
Ø 函数名中带有字符p
字符p是path的首字母,代表文件的绝对路径(或称相对路径),当函数名中带有字符p时,函数的参数就可以不用写出文件的相对路
径,只写出文件名即可,因为函数会自动搜索系统的path路径。例如,在运行可执行文件时,如果要输入相对路径“./a.out”,此处就可
以只输入可执行文件名“a.out”。
Ø 函数名中带有字符l
字符l是list的首字母,表示需要将新程序的每个命令行参数都当做一个参数传给它,参数个数是可变的,并且最后要输入一个NULL参
数,表示参数输入结束。
Ø 函数名中带有字符v
字符v是vector的首字母,表示该类函数支持使用参数数组,数组中的最后一个指针也要输入NULL参数,作为结束标志,这个参数数组
就类似于main函数的形参argv[]。
Ø 函数名以e结尾
字符e是environment的首字母,该类函数表示可以将一份新的环境变量表传给它。
在exec函数族中,execve函数是其余5个exec函数的基础,在exec函数族中,只有execve函数是经过系统调用的,其余5个函数在
执行时,都要在最后调用一次execve函数。
接下来通过几个exec函数族的实例,了解一下这些函数是如何实现的。
例7.3 在Linux系统中,使用vim编辑器编写两个程序,分别存放在execve.c文件中和new2.c文件中,用来演示如何使用execve函
数。(实例位置:光盘\TM\sl\7\3)
/****execve.c文件******/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
extern char **environ;
int main(int argc,char* argv[])
{
execve("new",argv,environ);
puts("正常情况下无法输出此信息!");
}
/*******new2.c文件*******/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main(void)
{
puts("welcome to mrsoft!");
return 0;
}
& 说明:execve函数所实现的功能就是创建一个子进程,在子进程里执行另一个文件。
?技巧:在演示这个execve.c程序时,首先要编译这两个文件,得到两个可执行文件execve和new,然后在shell中运
行“./execve”,这样这个实例就演示完成了。
本实例的运行结果如图7.6所示。
图7.6 execve函数的演示结果
在这个运行结果中,只输出了“welcome to mrsoft!”,说明在execve.c这个程序中执行了new2.c这个程序中的代码,那么
execve.c程序中的“正常情况下此信息无法输出”这句话为什么没有正常输出呢?
原因就是调用了execve函数,调用了这个函数后,将进程中的代码段、数据段和堆栈段都进行了修改,使得这个新创建的子进程只执行
了新加载的这个程序的代码,此时父进程与子进程的代码不再有任何关系。执行了execve函数后,原来存在的代码都被释放了,即
execve.c这个文件中的“puts(“正常情况下无法输出此信息!”)”代码已经执行不到,因此无法输出此信息。
如果在使用了execve函数后,后面的代码还想继续运行,不想因为这一个函数失去了整个函数的功能,那么可以采用ifelse
条件选择语
句,选择execve函数调用在子进程中,其余代码在父进程中执行。修改execve.c程序的代码如下:
/******修改execve.c文件*******/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
extern char **environ;
int main(int argc,char* argv[])
{
pid_t pid;
if((pid=fork())<0)
puts("create child process failed!");
if(pid==0)
execve("new",argv,environ);/*在子进程中调用execve函数*/
else
puts("此信息正常输出!"); /*父进程中输出此消息*/
}
U注意:由于调用fork函数输出父子进程中信息时,没有输出的先后顺序,由系统的调度决定,因此,输出信息无先后顺序。
修改后的程序的运行效果如图7.7所示。
图7.7 继续execve函数后的代码演示图
例7.4 在Linux系统中,演示execlp函数的使用方法。程序实现令程序中传递的第一个参数代表vim这个命令,打开一个文件。(实例
位置:光盘\TM\sl\7\4)
实现代码如下:
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main(int argc,char* argv[])
{
if(argc<2)
{
printf("vi的等效用法: %s filename\n",argv[0]);
return 1;
}
execlp("/bin/vi",”vi”,argv[1],(char*)NULL);
return 0;
}
在运行程序时,传入的第一个参数是可执行文件“./execlp”,传入的第二个参数为想要打开的文件的名称,程序的运行效果如图7.8所
示。
图7.8 在shell中的运行过程图
在上述界面中回车,就会进入到mrsoft.c这个文件中,效果如图7.9所示。
图7.9 mrsoft.c文件内容
& 说明:exec函数族的使用方法都大体相似,只是函数的参数各有不同,在此不一一进行介绍。
============================================================================
                                                                   进程等待
============================================================================

进程等待就是为了同步父进程和子进程,通常需要通过调用wait等待函数使父进程等待子进程结束。如果父进程没有调用等待函数,子进程就会进
入僵尸(Zombie)状态。
了解了等待函数的工作过程,就可以知道为什么没有调用等待函数时,子进程会进入僵尸状态。关于进入进程的等待状态,Linux系统提
供了如下等待函数。
wait函数的原型如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid,int *status,int options);
int waitid(idtype_t idtype,id_t id,siginfo_t *infop,int options);
在Linux系统的终端输入“man 2 wait”命令,可以查看关于wait函数的定义以及具体使用说明;
wait函数系统调用的工作过程是首先判断子进程时否存在,即是否成功创建了一个子进程,如果创建失败,子进程不存在,则会直接退出
进程,并且提示相关错误信息;如果创建成功,那么wait函数会将父进程挂起,直到子进程结束,并且返回结束时的状态和最后结束的子
进程的PID。
如果不存在子进程,提示的错误信息为
ECHILD,表示wait系统调用的进程,没有可以等待的子进程。
如果存在子进程,退出进程时的结束状态有(status)如下两种可能:
þ 子进程正常结束
当调用wait函数,子进程正常运行结束后,函数会返回子进程PID和status状态,此时的参数status所指向的状态变量就存放在子进程
的退出码中。退出码是所谓的从子进程的main函数中返回的值或者子进程中exit函数的参数。
þ 信号引起子进程结束
wait函数系统调用中,发送信号给进程,可能会导致进程结束运行。若发送的信号被进程捕获,就不会起到终止进程的作用;若信号没有
被进程捕获,则会使进程非正常结束。此时参数status返回的状态值为接收到的信号值,存放在最后一个字节中。
接下来,通过一个实例演示wait系统调用的作用。
在Linux系统中,演示wait()函数的使用方法。程序实现:输出在进程中调用wait()函数时正常退出的返回信息,以及接收到各种信号
时,返回的信息。
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
/*定义一个功能函数,通过返回的状态,判断进程是正常退出还是信号导致退出*/
void exit_s(int status)
{
if(WIFEXITED(status))
printf("normal exit,status=%d\n",WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("signal exit!status=%d\n",WTERMSIG(status));
}
int main(void)
{
pid_t pid,pid1;
int status;
if((pid=fork())<0) /*创建一个子进程*/
{
printf("child process error!\n");
exit(0);
}
else if(pid==0) /*子进程*/
{
printf("the child process!\n");
exit(2); /*调用exit退出函数正常退出*/
}
if(wait(&status)!=pid) /*在父进程中,调用wait函数等待子进程结束*/
{
printf("this is a parent process!\nwait error!\n");
exit(0);
}
exit_s(status); /*wait函数调用成功,调用自定义的功能函数,判断退出类型*/
/*又一次创建子进程,在子进程中,使用kill函数发送信号,导致退出*/
if((pid=fork())<0)
{
printf("child process error!\n");
exit(0);
}
else if(pid==0)
{
printf("the child process!\n");
pid1=getpid();
/*使用kill函数发送信号*/
// kill(pid1,9); /*结束进程*/
// kill(pid1,17); /*进入父进程*/
kill(pid1,19); /*终止进程*/
}
if(wait(&status)!=pid)
{
printf("this is a parent process!\nwait error!\n");
exit(0);
}
exit_s(status);
exit(0);
}
U注意:在上述代码中,加粗部分代码为3种不同的情况,分别表示信号类型为9、17和19时的3种情况,会产生3种不同的退出效果。
?技巧:在Linux系统的终端中输入“kill _l”命令,可以列出这些信号的具体情况,信号类型和其所对应的数字。
程序在3种不同情况下的运行结果,运行顺序为信号类型为9(SIGKILL)时、信号类型为17(SIGCHLD)时和信号类型为
19(SIGSTOP)时。
wait函数的演示效果
在Linux系统中提供了一些用于检测退出状态的宏,例如在上面的实例中,定义的功能函数exit_s()中用到的宏定义,下面介绍一下这些
宏定义的作用。
þ WIFEXITED(status)
宏定义的作用是当子进程正常退出时,返回真值。正常退出是指系统通过调用exit()和_exit()在main函数中返回。
þ WIFSIGNALED(status)
表示当子进程被没有捕获的信号终止时,返回真值。
þ WIFSTOPPED(status)
当子进程接收到停止信号时,返回真值。这种情况仅出现在调用waitpid()函数时使用了WUNTRACED选项。
þ WIFCONTINUED(status)
该宏表示当子进程接收到信号SIGCONT时,继续运行。
þ WEXITSTATUS(status)
返回子进程正常退出时的状态,该宏定义只适用于当WIFEXITED为真值时。
þ WTERMSIG(status)
用于子进程被信号终止的情况,返回此信号类型,该宏用于WIFSIGNALED为真值时。
þ WSTOPSIG(status)
返回使子进程停止的信号类型,该宏用于WIFSTOPPED为真值时。
关于进程等待函数,通过例7.4了解到了wait函数的使用方法,还有一个常用的等待函数waitpid(),该函数实现的功能与wait函数相
同,它们的区别在于:wait()函数用于等待所有子进程的结束;而waitpid()函数仅用于等待某个特定进程的结束,这个特定的进程是指
其PID与函数中的参数pid相关时。所谓的相关,有如下几种可能:
þ pid<1:
等待进程组ID等于pid绝对值的任一子进程时退出。
þ pid=1:
等待任意一个子进程退出。
þ pid=0:等待进程组ID等于调用进程的组ID的任一子进程时退出。
þ pid>0:等待进程ID等于pid的子进程退出
在waitpid()函数中,参数option的取值及其意义如下:
þ WNOHANG:该参数表示没有子进程退出就立即返回;
þ WUNTRACED:该参数表示若发现子进程处于僵尸状态,但未报告状态,则立即返回。
============================================================================
                                                                     进程退出
============================================================================
当想要中止或者结束一个进程时,会使用系统调用exit,正常退出进程,该系统调用包括exit()和_exit()两个函数,下面分别进行介绍。
þ exit函数
在终端中输入命令“man 3 exit”,可以显示exit()函数的原型以及其功能等信息。效果如图7.13所示。
通过终端中exit()函数的信息可以知道,exit()函数的原型为:
#include<stdlib.h>
void exit(int status);
注意:该函数调用成功与失败都没有返回值,并且也没有出错信息的提示。
exit()函数的作用是终止进程,并将运算status&0377表达式后的值返回给父进程,在父进程中可以通过wait()函数获得该值。
þ _exit函数
在终端中输入命令“man 2 exit”,会显示出_exit()函数的相关信息;
终端中_exit()函数的相关信息
_exit()函数实现的功能与exit()函数类似,都可以终止进程,该函数的原型为:
#include<unistd.h>
void _exit(int status);
_exit()函数与前面讲过的exit()函数相同,无论调用成功与否,都没有返回信息。
在前面讲述进程创建时,讲述了使用vfork函数创建的子进程在退出时,强调只能使用_exit()函数退出进程,而不能使用exit()函数退出
进程,这就是两个函数的区别所造成的。在调用exit()函数时,会对输入输出流进行刷新,释放所占用的资源以及清空缓冲区等;而
_exit()函数则不具备刷新缓冲区等操作的功能。
?技巧:在exit系统调用中,函数exit()在终止进程时,会关闭所有文件,清空缓冲区,因此如果在fork()函数和vfork()函数中使用
exit()函数终止子进程,会清空标准输入输出流,可能造成临时文件丢失,且vfork()函数是父子进程共享虚拟内存,如果在子进程中使用
exit()函数会严重影响到父进程,所以在使用这两个创建进程的函数时,尽量都不要使用exit()函数终止子进程。
& 说明:在上述关于进程创建、进程等待中均使用了exit()和_exit()函数退出进程,故在此对其不做举例说明。
============================================================================
进程组与时间片分配
============================================================================
进程组
所谓进程组,顾名思义,就是一个或者多个进程的集合。作为一个进程组,里面的每一个进程都有统一的一个进程标识,就如同每一个学
生,被分在一个一个的班级里,每一个班的学生都有一个共同的标识,例如高三4班的学生。
在Linux系统中,可以通过调用getpgrp()函数获取进程组ID,该函数的原型为:
#include<sys/types.h>
#include<unistd.h>
pid_t getpgrp(void);
调用该函数可以返回调用该函数的进程所在的进程组ID。在进程组中有一个特殊的进程,该进程的ID与进程组的ID相同。
每一个进程都有其生命期,从创建进程到进程终止,这是一个进程的生命期,而进程组的生命期是从该进程组的创建到最后一个进程终
止。在Linux系统中,可以使用setpgid()函数创建一个新的进程组或者将一个进程加入到一个进程组中,该函数的原型为:
#include<sys/types.h>
#include<unistd.h>
Int setpgid(pid_t pid,pid_t pgid);
当函数setpgid()调用成功,返回值为0,调用失败时,返回值为1

下面通过调用上述两个函数,掌握关于多个进程间组成的进程组的应用。
在Linux系统中,通过获取进程ID和获取进程组ID,创建一个新的进程组。
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main(void)
{
int a;
pid_t pgid,pid;
pid=(long)getpid();
pgid=(long)getpgrp();
a=setpgid(pid,pgid);
printf("a=%d,pid=%ld,pgid=%ld\n",a,pid,pgid);
return 0;
}
在该程序中,通过getpid()函数和getpgrp()函数,获取pid和gpid值,然后将两个ID值作为参数传递给函数setpgid(),使用该函数创
建一个新的进程。
创建新进程组
技巧:在setpgid()函数中,如果参数pid和参数gpid两者相等,则该函数的功能是创建一个新的进程组;如果两个值不同,并且pgdi是
一个已经存在的进程组,那么该函数的功能是将pid进程加入到pgid这个进程组中。
————————————————————————————————————————————
下面介绍一下关于多进程间时间片切换的调度策略。
(1)时间片轮转调度策略
时间片的轮转调度策略遵循先来先得的原则运行进程,将进程按先后顺序排成队,当调度开始时将CPU分配给首进程,当其执行完一个时
间片后,系统会发出信号,调度就会根据接收到的信号停止该进程,并将该进程放到队列的末尾,然后将CPU分配给下一个进程,同样运
行一个时间片,然后发出信号,调度会根据信号结束该进程,并将其送到队尾,继续重复上述步骤,这就是时间片的轮转调度策略。
(2)优先权调度策略
有些进程在运行时很紧迫,需要优先处理,因此在调度策略中引入了优先权调度算法,每一个进程都有其自己的优先级,优关于优先权调
度策略有两种方式。
一种是非抢占式优先权策略。这种调度策略系统将CPU分配给队列中优先权最高的进程后,会首先全速执行完该进程,或者该进程放弃
CPU时,再分配给另一个优先权高的进程。
另一种是抢占式优先权调度策略。该策略的原理是首先将CPU分配给当前优先级别最高的进程,然后每当出现一个新进程时,都会与正在
使用CPU的进程进行比较,若是新的进程优先级别高,则会触发进程调度,这个优先级别高的进程将会抢占CPU。
在Linux系统中,提供了几个函数,用于设置和获取进程的调度策略等信息。
在设置进程的调度策略时,可以通过参数pid确定需要设置的进程,通过参数policy设置调度策略,然后通过参数param保存进程的调度
参数,该函数的定义形式如下:
#include<sched.h>
int sched_setscheduler(pid_t pid,int policy,const struct sched_param *param);
int sched_getscheduler(pid_t pid);
说明:关于优先级调度策略,在Linux系统中也提供了一些关于优先级的操作函数,如nice()函数用于改变进程的动态优先级;
setpriority()函数和getpriority()函数用于设置和获取进程的动态优先级。
============================================================================
                                                                                线程
============================================================================
在Linux系统中,每一个线程都拥有一个自身的属性,用于代表该线程的特性,而一个进程中的多个线程,也有其共同的属性。
1.摧毁与初始化线程属性对象
当使用一个线程的属性对象前,需要首先初始化该对象,然后才可以对该线程的属性进行设置和修改。
初始化线程属性的函数原型为:
#include<pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
若该函数调用成功,返回值为0,调用失败,返回值为非0值。
pthread_attr_init()函数必须在创建线程函数之前调用,可以使用函数中参数attr中的属性来初始化线程属性的对象。
该摧毁属性对象的函数的原型为:
#include<pthread.h>
int pthread_attr_destroy(pthread_attr_t *attr);
pthread_attr_destroy()函数的功能是摧毁attr所指向的线程属性对象。销毁的attr属性对象可以使用上述初始化函数重新初始化。
参数attr是pthread_attr_t结构体类型的。该结构体类型中,定义了attr参数的属性,定义如下:
typedef struct
{
int__detachstate; /*线程的分离状态*/
int__schedpolicy; /*线程调度策略*/
struct sched_param__schedparam; /*线程的调度参数*/
int__inheritsched; /*线程的继承性*/
int__scope; /*线程的作用域*/
size_t__guardsize;
int__stackaddr_set;
void *__stackaddr; /*线程堆栈位置*/
unsigned long int__stacksize; /*线程堆栈大小*/
}pthread_attr_t;
在pthread_attr_t结构体类型中定义了上述线程属性,这些属性的意义如下:
þ detachstate
若表示线程的可连接状态,可以取值为PTHREAD_CREATE_JOINABLE;若表示线程的分离状态,可以取值为
PTHREAD_CREATE_DETACHED。
þ schedpolicy
该变量表示线程的调度策略,当取值为SCHED_OTHER时,属性表示普通,非实时的调度策略;若取值为SCHED_RR,属性表示实
时、轮转的调度策略;当变量取值为SCHED_FIFO时,属性表示实时、先进先出的调度策略。
þ schedparam
该变量代表线程的调度参数,该值由线程的调度策略决定。
þ inheritsched
表示线程的继承性。当取值为PTHREAD_EXPLICIT_SCHED时,表明是否从父线程处继承调度属性;当取值为
PTHEAD_INHERIT_SCHED时,表明从父进程继承。
þ scope
该变量表示线程的作用域,当取值为PTHREAD_SCOPE_SYSTEM时,表明每个线程占用一个系统时间片。
2.设置与获取线程的分离状态
关于线程的分离状态这个属性,其决定了线程是如何结束的。在默认情况下,线程是属于可连接状态的,这样线程在进程没有退出之前,
是不会释放线程所占用的资源的,此状态下,可以通过调用pthread_join函数来等待其他线程的结束,这样线程在结束之后,不会等待
进程退出,会自动释放掉自己的资源,而处于分离状态的线程在终止后会释放掉自身所占资源。
pthread_attr_setdetachstate()函数和pthread_attr_getdetachstate()函数是用来设置和获取线程的分离状态这个属性的。
这两个函数的原型为:
#include<pthread.h>
Int pthread_attr_setdetachstate(pthread_attr_t* attr,int detachstae);
Int pthread_attr_getdetachstate(pthread_attr_t*attr,int detachstate);
参数说明:
attr:作为设置线程属性的参数,attr指向要设置的线程属性对象的指针;作为获取线程属性的参数,attr作为从该参数中获取线程的分
离状态的信息。
detachstate:该参数取值为PTHREAD_CREATE_DETACHED和PTHREAD_CREATE_JOINABLE。在设置属性的函数中,将
属性设置为这两种属性,若在获取属性的函数中,detachstate参数所指向的内存中存放获取到的属性。
3.设置与获取线程属性对象的调度策略
在前面的介绍中,已经了解到了调度策略信息有SCHED_OTHER、SCHED_RR和SCHED_FIFO等3种形式。要对线程的这3中调度策
略属性进行设置和修改,需要用到如下函数:
#include<pthread.h>
Int pthread_attr_setschedpolicy(pthread_attr_t* attr,int policy);
Int pthread_attr_getschedpolicy (pthread_attr_t*attr,int policy);
在pthread_attr_setschedpolicy()设置属性函数中,参数attr指向要设置的线程属性对象的指针。参数policy为要设置的属性内容,
即要设置的调度策略形式。
在pthread_attr_getschedpolicy()获取属性函数中,从attr参数中获取线程调度策略信息,将结果赋给policy指针所指向的内存空间
中。
说明:关于线程相关属性的设置和获取,都有相应的函数进行操作,这里不做详细介绍。
============================================================================
                                                           获取与设置进程标识
============================================================================
获取进程标识
进程ID和父进程ID用PID来标识,在Linux系统中,init进程是所有进程的父进程,其PID值为1。
1.获得进程ID和父进程ID
在Linux系统中,可以通过在终端输入命令“ps”获取当前系统正在运行的进程的ID值。
当前正在运行的进程ID
如果在程序中想要获取进程ID,可以通过调用getpid()函数;如果要获取父进程ID,则可以调用getppid()函数。在终端中输入“man
getpid”命令,就可以查看这两个函数的相关介绍。
获取进程ID的函数介绍
使用两个函数需要引用两个头文件;getpid和getppid这两个函数的原型为:
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
两个函数调用成功时,会返回进程的ID值。
说明:pid_t数据类型表示非负整数值。这两个函数没有调用失败的可能。
在Linux系统中,演示getpid()函数和getppid()函数的使用方法。
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
printf("进程ID:%ld\n",(long)getpid());
printf("父进程ID:%ld\n",(long)getppid());
return 0;
}
获取当前程序的进程ID和其父进程ID
2.获得用户ID和有效用户ID
在Linux系统中,每一个用户都有其唯一的用户标识,即用户ID(UID),在/etc/passwd文件下可以获取到每一个用户ID。
用户标识(UID)
用户ID(UID)用于表示进程的创建者信息,有效用户ID(EUID)用于表示创建进程的用户,在任意时刻对资源和文件都具有访问权限。通
常情况下,UID和EUID的值是相同的。
在程序中,可以调用getuid()函数和geteuid()函数获取用户标识,在使用这两个函数之前需要引用以下两个头文件:
#include<sys/types.h>
#include<unistd.h>
这两个函数的定义形式如下:
uid_t getuid(void); /*获取当前进程的用户标识*/
uid_t geteuid(void); /*获取当前进程的有效用户标识*/
在程序中调用这两个函数时,如果调用成功就会返回标识值。说明:这两个函数没有调用失败的时候,总是调用成功。
在Linux系统中,演示getuid()函数和geteuid()函数的使用方法。
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main(void)
{
printf("UID=%ld\n",(long)getuid());
printf("EUID=%ld\n",(long)geteuid());
return 0;
}
获取用户标识和有效用户标识
& 说明:通过程序运行结果可以验证,这两个函数此时获取的用户标识和有效用户标识是相同的,都为502,与在/etc/passwd文件
中获取到的用户ID和有效用户ID是相同的。
3.获得组ID和有效组ID
进程的组标识GID和有效组标识EGID代表创建进程的用户组的信息以及用户组对于进程的访问权限的信息。
在Linux系统中,可以使用函数getgid()函数和getegid()函数获取当前进程的组ID和有效组ID
终端中GID和EGID的描述
两个获取进程信息的函数的返回值为gid_t类型的数据,该类型与pid_t和uid_t类型相同,都代表一个非负整数。两个函数总是调用成功
的,成功后返回当前进程的组信息和有效组信息。
在Linux系统中,使用getgid()和getegid()函数获取当前进程的组信息和有效组信息。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main(void)
{
printf("group ID=%d\n",(long)getgid());
printf("effective group ID=%d\n",(long)getegid());
return 0;
}
通过前面的介绍,了解了获取当前进程的各种ID的方法,另外,对于用户ID和用户组ID,还可以对其进行设置,修改用户标识值。
setuid()和setgid()函数可以设置用户标识和设置用户组标识。两个函数的原型为:
#include<sys/types.h>
#include<unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
setuid()函数用于修改当前进程用户标识,参数uid为设置的新的用户标识,setuid()函数若调用成功则返回值为0,否则返回1

通常情况下,在Linux系统中会有两个使用权限,一个是普通用户,一个是系统管理员(root,根用户),如果传递的参数是普通用户的
用户标识时,会成功将参数uid的值赋给进程UID。若是使用管理员的UID作为参数,为了确保系统的安全性,需要多加注意,该函数会
检查调用的有效用户ID,如果确认是管理员UID,那么所有与用户进程有关的ID都会被设置为参数uid值。不使用setuid()函数后,就又
会恢复管理员的权限。
而函数setgid()用于设置当前进程的有效用户组标识,如果调用该函数的用户是系统管理员,那么真实用户组ID和已保存用户组ID也会同
时被设置。但是该函数在修改发出调用进程的ID时,并不会检查用户的真实身份。若函数调用成功,则返回0;否则返回1

接下来通过一个实例,了解这两个函数的使用方法。
在Linux系统中,使用setuid()和setgid()函数设置发出调用进程的用户标识和用户组标识。
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
int flag1,flag2;
flag1=setuid(0);
flag2=setgid(500);
printf("flag1=%d\n,flag2=%d\n",flag1,flag2);
return 0;
}
设置用户ID和组ID
在上述代码中设置用户ID时,将参数uid值设为0,即根用户的标识,为了系统的安全起见,调用该函数设置用户标识为0失败,因此返回
值为1;
而用户组标识设置为500,函数调用成功返回0。
============================================================================
                                                         通信:管道与命名管道
============================================================================
管道由Linux系统提供的pipe()函数创建,该函数的原型为:
#include<unistd.h>
int pipe(int filedes[2]);
pipe()函数用于在内核中创建一个管道,该管道一端用于读取管道中的数据,另一端用于将数据写入到管道中。
在创建一个管道之后,会获得一对文件描述符,用于读取和写入。然后将参数数组filedes中的两个值,传递给获取到的两个文件描述
符,filedes[0]指向管道的读端,filedes[1]指向管道的写端。
pipe()函数调用成功,返回值为0;否则返回1,
并且设置了适当的错误返回信息,返回信息如下:
EFAULT:参数filedes非法。
EMFILE:进程中使用了过多的文件描述。
ENFILE:打开的文件达到了系统允许的最大值。
pipe()函数只是创建了管道,要想从管道中读取数据或者向管道中写入数据,需要使用read()函数和write()函数从管道进行读写操作。
当管道通信结束后,需要使用close()函数关闭管道的两端,即读端和写端。
调用pipe()函数实现创建两个进程间的管道通信,pipe()函数只允许两个有联系的进程进行通信,如父子进程或者兄弟进程,因此首先需
要使用fork函数创建一个或者两个新的进程,然后再父子或兄弟进程之间进行数据的传递。
Linux系统中管道是半双工模式的通信,即使用管道通信只能进行单向传递,也就是管道总共有两端,一端只能用于写入数据,其文件描
述符为filedes[1];另一端则只能用于读取数据,其文件描述符为 filedes[0]。
在Linux系统中,调用pipe()函数创建一个管道,实现管道的单向通信。
(1)在父进程中调用pipe()函数创建一个管道,产生一个文件描述符filedes[0]指向管道的读端和一个文件描述符filedes[1]指向管道
的写端。
(2)在父进程中调用一个fork()函数创建一个一摸一样的新进程,也就是所谓的子进程。父进程的文件描述符一个指向管道读端,另一
个指向管道的写端,同样,子进程也如此。
(3)在父进程关闭指向管道写端的文件描述符filedes[1],在子进程中,关闭指向管道读端的文件描述符filedes[0],此时,就可以将
父进程中的某个数据写入管道,然后再子进程中,将此数据读取出来。
(4)这样一个简单的单向通信就实现了。
单向通信的实现过程代码如下:
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#define MAXSIZE 100
int main(void)
{
int fd[2],pid,line;
char message[MAXSIZE];
/*创建管道*/
if(pipe(fd)==-1)
{
perror("create pipe failed!");
return 1;
}
/*创建新进程*/
else if((pid=fork())<0)
{
perror("not create a new process!");
return 1;
}
/*子进程*/
else if(pid==0)
{
close(fd[0]);
printf("child process send message!\n");
write(fd[1],"Welcome to mrsoft!",19); /*向文件中写入数据*/
}
else
{
close(fd[1]);
printf("parent process receive message is:\n ");
line=read(fd[0],message,MAXSIZE); /*读取消息,返回消息长度*/
write(STDOUT_FILENO,message,line); /*将消息写入终端*/
printf("\n");
wait(NULL);
_exit(0);
}
return 0;
}
NOte: 要想在Linux中使用管道实现双向通信,可以调用pipe函数创建两个管道,一个管道用于从父进程写,子进程读取;另一个管道
用于从子进程中写,父进程中读取数据。
________________________________________________________________________________________
在程序中可以调用mkfifo()函数创建一个命名管道文件,该函数的原型为:
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char* pathname,mode_t mode);
该函数的参数pathname是一个文件的路径名,是创建的一个命名管道的文件名;参数mode是指文件的权限,文件的权限值取决于
(mode&~umask)值。
使用mkfifo()函数创建的命名管道文件与前面介绍的管道通信相似,只是它们的创建方式不同,访问命名管道文件与访问文件系统中的其
他文件一样,都是需要首先打开文件,然后对文件进行读写数据。如果在命名管道文件中读取数据时,并没有其他进程向命名管道文件中
写入数据,则会出现进程阻塞状态;如果在写入数据的同时,没有进程从命名管道中读取数据,也会出现进程阻塞状态。
在Linux系统中,使用mkfifo函数创建命名管道,并最终实现在命名管道中传递数据:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#define FIFO "/home/cff/8/fifo4" /*宏定义一个命名管道文件名称*/
int main(void)
{
int fd;
int pid;
char r_msg[BUFSIZ];
if((pid=mkfifo(FIFO,0777))==-1) /*创建命名管道*/
{
perror("create fifo channel failed!");
return 1;
}
else
printf("create success!\n");
fd=open(FIFO,O_RDWR); /*打开命名管道*/
if(fd==-1)
{
perror("cannot open the FIFO");
return 1;
}
if(write(fd,"hello world",12)==-1) /*写入消息*/
{
perror("write data error!");
return 1;
}
else
printf("write data success!\n");
if(read(fd,r_msg,BUFSIZ)==-1) /*读取消息*/
{
perror("read error!");
return 1;
}
else
printf("the receive data is %s!\n",r_msg);
close(fd); /*关闭文件*/
return 0;
}
通过以上代码,可以掌握使用mkfifo函数创建命名管道,并进行数据传递的过程,具体如下:
(1)使用mkfifo函数创建一个命名管道,命名管道文件的路径名称是“/home/cff/8/fifo4”。
(2)调用open函数打开该命名管道文件,以读写的方式打开。
(3)然后调用write函数向文件中写入信息“hello world”,同时,调用read函数读取出该文件,输出到终端。
(4)调用close函数关闭打开的命名管道文件。
注意:在使用open函数打开文件后,数据的读与写要同步,否则会出现进程阻塞。导致程序无法继续运行。
============================================================================
                                                            通信:共享内存
============================================================================
ipc_perm结构体的定义如下:
struct ipc_perm
{
uid_t uid; /*拥有者的有效用户ID*/
gid_t gid; /*拥有者的有效组ID*/
uid_t cuid; /*创建者的有效用户ID*/
gid_t cgid; /*创建者的有效组ID*/
mode_t mode; /*访问权限*/

shmid_ds结构体定义形式如下:
struct shmidds
{
struct ipc_perm shm_perm; /*共享内存的ipc_perm结构对象*/
int shm_segsz; /*共享内存区域字节大小*/
ushort shm_lkcnt; /*共享内存区域被锁定的时间数*/
pid_t shm_cpid; /*创建该共享内存的进程ID*/
pid_t shm_lpid; /*最近一次调用shmop函数的进程ID*/
ulong shm_nattch; /*使用该共享内存的进程数*/
time_t shm_atime; /*最近一次附加操作的时间*/
time_t shm_dtime; /*最近一次分离操作的时间*/
time_t shm_ctime; /*最近一次改变的时间*/

1.shmget函数
在使用共享内存实现进程间通信时,需要首先调用shmget函数创建一块共享内存区域,如果已经存在了一块共享内存区域,那么,该函
数可以打开这个已经存在的共享内存。
该函数的定义形式如下:
#include<sys/ipc.h>
#include<sys/shm.h>
Int shmget(key_t key,size_t size,int shmflg);
参数说明:
key:共享内存的键值;
size:表示新创建的共享内存区域的大小,以字节表示;
shmflg:用于设置共享内存的访问权限,也表示调用函数的操作类型。
shmget函数的功能主要随着3个参数的不同搭配,会起到不同的作用。如:
þ 当key取值为IPC_PRIVATE,或不去此值,并且系统内核中没有与参数key表示的键值相对应的共享内存区域存在,shmflg会取
IPC_CREAT值,创建一个新的共享内存区域,参数size设置该共享内存区域的大小。
þ 如果key值不为IPC_PRIVATE时,shmflg参数设置为IPC_CREAT,并且已经存在一个与key值相对应的共享内存区域,那么该
函数会打开这个key值指定的区域。
þ 如果内核中存在与key值相关的内存区域,并且shmflg参数取值为(IPC_CREAT|IPC_EXCL),那么,该函数就会调用失败。
该函数如果调用成功,返回值是与参数key相关的共享内存区域的标识符;如果调用失败,返回值为1

2.shmat函数
shmat函数的功能是将共享内存区域附加到指定进程的地址空间中。该函数的定义形式如下:
#include<sys/types.h>
#include<sys/shm.h>
void *shmat(int shmid,const void *shmaddr,int shmflg);
参数说明:
shmid:共享内存的标识符;
shmaddr:指定进程的内存地址;
shmflg:表示该函数的操作方式。
如果shmat函数调用成功,则返回指向该共享内存区域的指针;调用失败,返回值为1

shmat函数的3个参数之间遵循如下约定:
þ 参数shmaddr为NULL,系统会自动选择一个合适的内存地址,将共享内存区域附加到此地址上。
þ 参数shmaddr不为NULL,并且shmflg参数指定了SHM_RND值时,函数会将共享内存区域附加到(shmaddr(
add mod
SHMLBA))计算所得的地址中。
U注意:SHM_RND表示取整,SHMLBA表示地辩解地址的整数倍。
3.shmdt函数
shmdt函数的功能是当某一进程不在使用该内存区域时,将使用shmat函数附加的共享内存区域,从该进程的地址空间中分离出来。
该函数的定义形式如下:
#include<sys/types.h>
#include<sys/shm.h>
Int shmdt(const void *shmaddr);
该函数调用成功时,返回值为0;如果调用失败,返回值为1

参数shmaddr为调用shmat函数附加成功时,返回的地址指针。该函数主要实现从shmaddr指针所指的地址空间中分离出此共享内存
区域,此共享内存区域仍然存在。
4.shmctl函数
shmctl函数主要实现了对共享内存区域的多种控制操作。该函数的定义形式如下:
#include<sys/ipc.h>
#include<sys/shm.h>
Int shmctl(int shmid,int cmd,struct shmid_ds *buf);
shmctl函数主要通过cmd参数设置的控制信息,对参数shmid提供的标识符指定的共享内存区域进行控制。
参数buf是一个指向shmid_ds结构体类型的指针。
在Linux系统中,参数cmd有如下8种控制信息,分别如下:
þ IPC_STAT
在内核中,将与标识符shmid相关的共享内存的数据复制到buf指向的共享内存区域中。
þ IPC_SET
根据参数buf指向的shmid_ds结构中的值设置shmid标识符所指的共享内存的相关属性。
þ IPC_RMID
删除shmid标识符所指的共享内存区域。
& 说明:该参数值要求必须确保共享内存被最终删除,否则共享内存所占用的空间将不能被释放。
þ IPC_INFO
此值为Linux系统中特有的参数值,用于获取关于系统共享内存限制和buf指向的相关参数的信息。
þ SHM_INFO
此值为Linux系统中特有的参数值,用于获取一个shm_info结构共享内存消耗的系统资源信息。
þ SHM_STAT
此值为Linux系统中特有的参数值,功能与IPC_STAT相同,但是参数shmid在这里不代表共享内存的标识符,而是一个内核中维持所有
共享内存区域信息的数组的索引值。
þ SHM_LOCK
此值为Linux系统中特有的参数值,用于阻止共享内存区域的交换。
þ SHM_UNLOCK
此值为Linux系统中特有的参数值,解锁共享内存区域。
在Linux系统中,使用shmget函数创建一个共享内存区域,在这个共享内存区域中写入字符串“welcome to mrsoft!”,然后在父子
进程中分别读取共享内存中的数据,进而实现了进程间的数据交换操作:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
int main()
{
int shmid;
int proj_id;
key_t key;
int size;
char *addr;
pid_t pid;
key=IPC_PRIVATE;
shmid=shmget(key,1024,IPC_CREAT|0660); /*创建共享内存*/
if(shmid==-1)
{
perror("create share memory failed!");
return 1;
}
addr=(char*)shmat(shmid,NULL,0);
if(addr==(char *)(-1))
{
perror("cannot attach!");
return 1;
}
printf("share memory segment's address:%x\n",addr);
strcpy(addr,"welcome to mrsoft!");
pid=fork();
if(pid==-1)
{
perror("error!!!!");
return 1;
}
else if(pid==0)
{
printf("child process string is' %s'\n",addr);
_exit(0);
}
else
{
wait(NULL);
printf("parent process string is '%s'\n",addr);
if(shmdt(addr)==-1)
{
perror("release failed!");
return 1;
}
if(shmctl(shmid,IPC_RMID,NULL)==-1)
{
perror("failed!");
return 1;
}
}
return 0;
}
============================================================================
                                                               通信:信号量
============================================================================
信号量的工作原理是:当有一个进程要求使用某一共享内存中资源时,系统会首先判断该资源的信号量,也就是统计的可以访问该资源的
单元个数,如果系统判断出该资源信号量值大于0,进程就可以使用该资源,并且信号量要减1,当不再使用该资源时,信号量再加1,方
便其他用户使用时,系统对其进行准确的判断。如果该资源的信号量等于0时,进程会进入休眠状态,等候该资源有人使用结束,信号量
大于0,这样进程就会被唤醒,对该资源进行访问。
在一个进程间通信机制中,信号量由多个信号组成,进程通过一个信号集实现同步。因此通常将信号量称之为信号量集。一个信号量集有
与其相对应的结构,用于定义信号量集的对象,这个结构存储了信号量集的各种属性。其定义形式如下:
struct semid_ds
{
struct ipc_perm *sem_perm; /*ipc_perm结构指针*/
struct sem *sem_base; /*sem结构指针*/
ushort sem_nsems; /*信号量个数*/
time_t sem_otime; /*最近一次调用semop函数的时间*/
time_t sem_ctime; /*最近一次改变该信号量*/

sem结构体类型中定义了信号量的一些信息,定义内容如下:
struct sem
{
ushort semval; /*信号量值*/
pid_t sempid; /*最近一次访问资源的进程ID*/
ushort semncnt; /*等待可用资源出现的进程数*/
ushort semzcnt; /*等待全部资源可被独占的进程数*/

1.创建信号量函数semget()
在使用信号量控制进程间同步时,需要首先创建一个信号量集,semget函数实现了创建一个新的信号量集操作和打开一个已经存在的信
号量集的操作,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semget(key_t key,int nsems,int semflg);
参数说明:
key:表示所创建的信号量集的键值。
nsems:表示信号量集中信号量的个数。当函数semget实现的作用是创建一个新的信号量集时,该参数才有效。
semflg:用于设置信号量集的访问权限也可表示该函数的操作类型。
如果该函数调用成功,返回值为与参数key相关联的标识符;如果调用失败,返回值为1

& 说明:创建信号量集的semget函数中参数所遵循的原则与创建共享内存的函数shmget函数类似。
2.信号量集操作函数semop()
semop函数实现的功能是对信号量集中的信号量进行操作。具体的操作内容与该函数的参数的设定有关,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
int semop(int semid,struct sembuf*sops,unsigned nsops);
参数说明:
semid:表示要进行操作的信号量集的标识符。
sops:为sembuf结构体指针变量,semop函数通过此参数指定对单个信号量的操作行为。
nsops:代表要操作的信号量。
参数sops的数据类型为sembuf结构体,此结构体中定义的主要元素包括如下几个:
unsigned short sem_num; /*信号量值*/
short sem_op; /*信号的操作*/
short sem_flg; /*操作标识*/
sem_op变量值根据其取值的范围确定执行的操作行为,如该值若大于0,则需要释放掉资源;如果小于0,则要获取共享资源,若为0表
示资源都已经处于使用状态。
sem_flg变量值作为操作的标识,与此函数相关的标识有IPC_NOWAIT和SET_UNDO。
3.信号量集的控制函数semctl()
对信号量集的控制主要通过函数semctl()实现,例如,通常在使用信号量集时,都要对信号量集中的元素进行初始化,semctl()控制函
数就可以实现此功能。
semctl()函数的原型为:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
Int semctl(int semid,int semnum,int cmd,…);
semctl函数的参数semid表示要修改的信号量集的标识;参数semnum表示需要修改的信号量集中的信号量个数;参数cmd表示该函
数的控制类型。该函数在参数中使用了省略号,表示参数个数未固定,该函数可能有3个或者4个参数,参数的个数取决于参数cmd的控
制类型取值,第4个参数为arg,用于读取或存储函数返回的结果,该参数是semun的共用体类型,此共用体的定义形式如下:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
semctl函数如果调用成功,返回值为0;如果调用失败,返回值为1;
如果参数cmd的取值为以下几种形式,返回值为指定的信息。
cmd的取值形式如下:
þ GETNCNT:返回值为semncnt的取值。
þ GETPID:返回值为sempid的取值。
þ GETVAL:返回值为semval的取值。
þ GETZCNT:返回值为semzcnt的取值。
þ IPC_INFO:返回值为内核中信号量集数组的最高索引值。
þ SEM_INFO:与IPC_INFO相同。
þ SEM_STAT:返回值为semid指定的标识符。
在Linux系统中,使用信号量集实现对共享资源的互斥访问,即同一时刻只允许一个进程对共享资源访问。
在sl1.c文件中,创建信号量集,模拟系统分配资源,假设系统中总共有4个资源可以使用,每隔3秒就有一个资源会被占用。相关代码如
下:
#include <sys/types.h>
#include <linux/sem.h>
#include<stdlib.h>
#include<stdio.h>
#define RESOURCE 4
int main(void)
{
key_t key;
int semid;
struct sembuf sbuf = {0,-1,IPC_NOWAIT};
union semun arg;
if ((key = ftok("/home/cff",'c')) == -1) /*创建新进程*/
{
perror("ftok error!\n");
exit(1);
}
if ((semid = semget(key,1,IPC_CREAT|0666)) == -1)
{
perror("semget error!\n");
exit(1);
}
arg.val = RESOURCE;
printf("可使用共资源有 %d 个!\n",arg.val);
if (semctl(semid,0,SETVAL,arg) == -1)
{
perror("semctl error!\n");
exit(1);
}
while (1)
{
if (semop(semid,&sbuf,1) == -1)
{
perror("semop error!\n");
exit(1);
}
sleep(3);
}
semctl(semid,0,IPC_RMID,0);
exit(0);
}
在sl2.c文件中,根据semop()函数,检测是否有资源可以利用,并且返回可使用资源的个数。相关代码如下:
#include <sys/types.h>
#include <linux/sem.h>
#include<stdlib.h>
#include<stdio.h>
int main(void)
{
key_t key;
int semid,semval;
union semun arg;
if ((key = ftok("/home/cff",'c')) == -1)
{
perror("key error!\n");
exit(1);
}
/*open signal */
if ((semid = semget(key,1,IPC_CREAT|0666)) == -1)
{
perror("semget error!\n");
exit(1);
}
while(1)
{
if ((semval = semctl(semid,0,GETVAL,0)) == -1)
{
perror("semctl error!\n");
exit(1);
}
if (semval > 0)
{
printf("还有 %d 个资源可以使用!\n",semval);
}
else
{
printf("没有资源可以使用!\n");
break;
}
sleep(3);
}
exit(0);
}
运行这个信号量模拟系统分配资源的项目时,分别在两个终端中编译并执行sl1.c文件和sl2.c文件,由于每隔3秒就会判断一次,所以在
运行两个可执行文件时时间间隔不要太长。
============================================================================
                                                               通信:消息队列
============================================================================
消息队列是一种通过链表结构组织的一组消息,消息是链表中具有一定格式及优先级的数据记录。消息队列与其他两种进程间通信对象
(共享内存、信号量)相同,都存放在内核中,多个进程通过消息队列的标识符对消息数据进行传送,实现进程间的通信。
每一个消息队列都有一个与之相对应的结构,用于定义一个消息队列的对象,该结构体类型的定义形式如下:
Struct msqid_ds
{
Struct ipc_perm msg_perm; /*消息队列的指向ipc_perm结构的指针*/
Struct msg *msg_first; /*指向消息队列中第一个消息的指针*/
Struct msg *mst_last; /*指向消息队列最后一个消息的指针*/
Ulong msg_ctypes; /*当前消息队列的总字节数*/
Ulong msg_qnum; /*总消息数量*/
Ulong msg_qbytes; /*消息队列中字节数的上限*/
Pid_t msg_lspid; /*最后一个调用msgsnd()函数的进程ID*/
Pid_t msg_lrpid; /*最后一个调用msgrcv()函数的进程ID*/
Time_t msg_stime; /*最后一次调用msgsnd()函数的时间*/
Time_t msg_rtime; /*最后一次调用msgrcv()函数的时间*/
Time_t msg_ctime; /*最后一次改变该消息队列的时间*/
& 说明:在不同的系统中,3种IPC对象相对应的结构体类型都会有不同的新成员,在本章中介绍的结构体类型中的成员只是关键成
员。
消息队列的相关操作
使用消息队列实现进程间通信,需要首先调用msgget()函数创建一个消息队列,然后调用msgsnd()函数向该消息队列中发送指定的消
息,通过msgrcv()函数接收该消息,最后调用msgctl()函数对消息队列进行指定的控制操作,这样一个使用消息队列实现进程间的通信
就是先了。接下来对上述应用到的这些消息队列的操作函数进行介绍。
1.msgget()函数
msgget()函数实现了创建一个新的消息队列或打开一个已经存在的消息队列,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgget(key_t key,int msgflg);
参数说明:
key:表示所创建的消息队列的键值。
msgflg:用于设置消息队列的访问权限也可表示该函数的操作类型。
如果该函数调用成功,返回值为与参数key相关联的标识符;如果调用失败,返回值为1

& 说明:创建消息队列的msgget()函数中参数所遵循的原则与创建共享内存的函数shmget()函数类似。
调用msgget()函数创建一个消息队列时,与消息队列相对应的msqid_ds结构体中的成员会被初始化。
2.msgsnd()函数
msgsnd()函数实现的功能是向消息队列中发送消息,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgsnd(int msqid,const void *msgp,size_t msgsz,int msgflg);
参数说明:
msqid:将信息发送到的消息队列的标识符。
msgp:指向要发送的消息数据。
msgsz:以字节数表示的消息数据的长度。
msgflg:消息队列满时的处理方法,该参数可0值或IPC_NOWAIT。
该函数调用成功时,返回值为0;调用失败,返回值为1

要发送的消息存放在msgbuf结构体中,使用msgp指针指向该类型引用消息数据的内容和消息的类型。msgbuf结构体的定义形式如
下:
struct msgbuf
{
long mtype; /*消息类型,以大于0的整数表示*/
char mtext[1]; /*消息内容*/
};
如果消息队列中有足够的空间,msgsnd函数会立即返回,并实现发送消息到msgp指定的消息队列中。
如果消息队列满时,msgflg参数没有设置IPC_NOWAIT值,则msgsnd()函数将阻塞;如果设置了IPC_NOWAIT值,msgsnd()函数
将调用失败,返回1,
直到消息队列中有空间时,函数会返回0。
3.msgrcv()函数
msgrcv()函数实现的功能是接收消息队列中的消息数据,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
参数说明:
msqid:从msqid标识符所代表的消息队列中接收消息。
msgp:指向存放消息数据的内存空间。
msgsz:以字节数表示的消息数据的长度。
msgtyp:读取的消息数据的类型。
msgflg:对读取的消息数据不满足情况时的处理。
该函数调用成功时,返回值为0;调用失败,返回值为1

& 说明:关于参数msgflg的取值情况请参照相应系统手册的详细说明。也可以在Linux系统的终端中输入命令“man msgrcv”进行
查看。
4.msgctl()函数
msgctl()函数主要实现对消息队列的控制操作,该函数的定义形式如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
msgctl()函数实现对参数msqid标识符所指定的消息队列进行控制,控制内容取决于参数cmd的取值。参数buf指针指向需要执行控制
操作的消息队列。
参数cmd的部分取值如下:
þ IPC_STAT
在内核中将与标识符msqid相关的消息队列的数据复制到buf指向的消息队列中,调用进程必须对消息队列有读的权限。
þ IPC_SET
根据参数buf指向的shmid_ds结构中的值设置msqid标识符所指的消息队列中的相关属性。
þ IPC_RMID
删除msgqid标识符所指的消息队列,唤醒所有等待的读和写进程。
& 说明:使用该值时,该进程必须具备相应的访问权限,或者该进程的EUID与消息队列的创建者相同,或者与消息队列的所有者一
样。
þ IPC_INFO
此值为Linux系统特有的参数值,用于获取系统级别的消息队列限制,保存在buf指向的缓冲区中。
& 说明:buf指针指向一个msginfo结构体类型,该结构体类型中定义了消息队列的详细信息。
掌握了关于消息队列的相关操作后,通过消息队列实现进程间通信就不再那么盲目,接下来,通过一个实例讲解消息队列是如何实现进程
间通信的。
在Linux系统中,使用msgget()函数创建一个消息队列,通过msgsnd()函数发送两次消息,第一次发送的消息内容为“hello
mrsoft!”,第二次发送的消息为“goodbye!”。接下来调用msgrcv()函数接收消息,这样就实现了一个消息队列的进程间通信。
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
key_t key;
int proj_id=1;
int msqid;
char message1[]={"hello mrsoft!"};
char message2[]={"goodbye!"};
struct msgbuf
{
long msgtype;
char msgtext[1024];
}snd,rcv;
key=ftok("/home/cff/2",proj_id); /*创建新进程*/
if(key==-1)
{
perror("create key error!");
return 1;
}
if((msqid=msgget(key,IPC_CREAT|0666))==-1)/*创建消息队列*/
{
printf("magget error!\n");
exit(1);
}
snd.msgtype=1;
sprintf(snd.msgtext,message1);
if(msgsnd(msqid,(struct msgbuf *)&snd,sizeof(message1)+1,0)==-1)
{
printf("msgsnd error!\n");
exit(1);
}
snd.msgtype=2;
sprintf(snd.msgtext,"%s",message2);
if(msgsnd(msqid,(struct msgbuf *)&snd,sizeof(message2)+1,0)==-1)
{
printf("msgrcv error!\n");
exit(1);
}
if(msgrcv(msqid,(struct msgbuf *)&rcv,80,1,IPC_NOWAIT)==-1)
{
printf("msgrcv error!\n");
exit(1);
}
printf("the received message:%s.\n",rcv.msgtext);
//msgctl(msgid,IPC_RMID,0); /*删除新创建的消息队列*/
system("ipcs -q"); /*在系统中显示新创建的消息队列的信息*/
exit(0);
}
在此函数中,调用接收函数msgrcv()时,设置了接收的消息的类型为“1”,因此接收到的信息为第一个“hello mrsoft!”,并且,在程
序中调用了system()函数,执行显示系统中新创建的消息队列的信息。
消息队列实现进程间通信
U注意:程序中使用ftok()函数产生key值,如果在运行过一次程序后,再次运行此程序会出现错误信息提示。因为与key值相关的消息
队列标识符在运行过一次程序后就已经存在了,所以再次使用与key值相关的此标识符,会产生错误。因此在使用该程序时,可以使用
msgctl()函数删除新创建的消息队列,方便下次运行程序不再出错。
============================================================================
                                                 目录文件、链接文件、设备文件
============================================================================
1.获取当前的工作目录
在Linux系统中提供了一个系统调用函数getcwd(),用于获取当前的工作目录。每一个进程都有一个当前的工作目录这个概念,当前的
工作目录就是一个路径名的解析。
函数getcwd()的定义形式如下:
#include<unistd.h>
char *getcwd(char *buf,size_t size);
参数buf用于存储当前工作目录的字符串;参数size用于存放字符串的大小。
函数如果调用成功,返回指向当前工作目录字符串的指针,否则返回NULL,并设置适当的errno值。
使用getcwd()函数获取当前进程的工作目录。程序代码如下:
#include<stdio.h>
#include<unistd.h>
#include<limits.h>
int main()
{
char a[PATH_MAX]; /*存放工作目录的字符串*/
if(getcwd(a,PATH_MAX)==NULL) /*获取当前工作目录*/
{
perror("getcwd failed!");
return 1;
}
printf("输出当前工作目录:%s\n",a); /*输出字符数组*/
return 0;
}
2.更改当前工作的目录
在实际应用中,有时需要更改当前的工作目录,在系统中提供了函数chdir()和函数fchdir()更改当前的工作目录。
这两个函数的定义形式如下:
#include<unistd.h>
Int chdir(const char *path);
Int fchdir(int fd);
参数path指的是文件的相对路径,参数fd指的是文件的文件描述符。
这两个函数都是根据相对路径或者文件描述符指定某一文件,对指定的这个文件更改当前工作目录。
函数调用成功时,返回值为0;否则返回值为1,
并设置适当的errno值。
3.创建和删除目录
系统中提供了函数mkdir()创建文件目录,并且还提供了函数rmdir()删除指定文件的目录。
创建文件目录的函数的定义形式如下:
#include<sys/stat.h>
#include<sys/types.h>
Int mkdir(const char *pathname,mode_t mode);
参数pathname为需要创建的文件目录的名称;参数mode指的是创建目录的访问权限,该权限取决于(mode&~umask&0777)
值。
函数调用成功返回值为0;否则返回值为1,
并设置适当的errno值。
使用mkdir()函数创建一个新的工作目录,名为“/home/cff/9/hello“;程序代码如下:
#include<sys/stat.h>
#include<sys/types.h>
#include<stdio.h>
int main()
{
char* dir="/home/cff/9/hello";/*创建的新目录*/
if(mkdir(dir,0700)==-1) /*调用创建新目录的函数*/
{
perror("create failed!");
return 1;
}
printf("create hello successful!\n");
return 0;
}
& 说明:创建的新目录的访问权限为所有者具有的所有权限,而同组和其他用户没有权限。代码中权限处的值为0700,表示八进制数
700,该值取决于(mode&~umask&0777)的计算值,umask的值为默认权限,其值固定为0002,经计算得最终权限值为
0700。
可以创建一个新的目录自然也可以删除一个目录,在Linux系统中提供了rmdir()函数用于删除一个指定的目录。该函数的定义形式如
下:
#include<unistd.h>
Int rmdir(const char *pathname);
参数pathname代表的是指定要删除的目录。如果该函数调用成功,返回值为0,否则返回值为1,
并设置适当的errno值。
使用rmdir()函数删除刚刚创建的目录“/home/cff/9/hello”。程序代码如下:
#include<stdio.h>
#include<unistd.h>
int main()
{
char* dir="/home/cff/9/hello";
if(rmdir(dir)==-1)
{
perror("failed!");
return 1;
}
printf("remove successful!\n");
return 0;
}
4.打开与关闭文件
在Linux系统中,目录文件作为一种特殊的文件,可以被打开,关闭以及读取。系统提供了系统调用函数opendir()和函数closedir()用
于打开目录文件和关闭目录文件。就像对普通文件的操作一样,打开之后,当不再使用,需要及时关闭文件,否则会造成文件的丢失。
这两个函数的定义形式如下:
#include<sys/types.h>
#include<dirent.h>
DIR *opendir(const char *name);
int closedir(DIR *dir);
参数name指的是需要打开的目录文件的名称;参数dir指的是想要关闭的目录流。
函数opendir()调用成功时,返回DIR*形态的目录流,接下来对目录的读取和搜索都要使用此返回值。调用失败时,返回NULL。
函数closedir()调用成功时,返回值为0;否则返回值为1,
并设置相应的errno值。
5.读取目录文件
对目录文件打开后,必然要对该文件进行读取等操作,因此系统提供了系统调用函数readdir(),用于读取目录文件中的数据。
函数readdir()的定义形式如下:
#include<sys/types.h>
#include<dirent.h>
struct dirent *readdir(DIR *dir);
参数dir用于存放目录流。
readdir()返回参数dir目录流的下个目录的进入点。
函数的返回值为下个目录进入点,数据类型为dirent结构体,该结构体的定义形式如下:
struct dirent
{
ino_t d_ino; /*此目录进入点的索引号inode*/
off_t d_off; /*目录文件开头到此目录进入点的位移*/
unsigned short d_reclen; /*目录名长度,不包括NULL字符*/
unsigned char d_type; /*文件的类型*/
char d_name[256]; /*文件名*/

函数调用成功则返回下个目录进入点。调用失败或读取到目录文件尾则返回NULL。
通过opendir()函数打开目录文件“/home/cff/9”,然后调用readdir()函数读取该目录中的数据,最后调用closedir()函数关闭该目录
文件。程序代码如下:
#include<dirent.h>
#include<unistd.h>
#include<stdio.h>
main()
{
DIR * dir;
struct dirent * ptr;
int i;
dir =opendir("/home/cff/9");/*打开的目录文件*/
while((ptr = readdir(dir))!=NULL)/*读取该目录文件中的数据*/
{
printf("d_name: %s\n",ptr->d_name);/*输出文件的名字*/
}
closedir(dir); /*关闭目录文件*/
}
1.硬链接
硬链接是依附于索引节点而存在的,在Linux系统中,使用硬链接,需要注意如下几点:
(1)目录无法创建硬链接,只有文件才可以创建硬链接。
(2)硬链接不能够跨越文件系统,即不能为处在不同分区上的文件创建硬链接。
在Linux系统的终端下,可以通过ln命令创建一个文件的硬链接。链接文件相当于原文件的一个快捷方式,两个文件的索引节点值是一致
的,当删除原文件时,硬链接文件依然指向原来的索引节点值,即索引节点没有被删除,因此想要删除文件的数据,需要将文件以及所有
的硬链接一同删除。
在Linux系统中提供了相关的系统调用函数创建一个新的硬链接和解除一个硬链接。
þ 创建硬链接函数link()
系统调用函数link()的定义形式如下:
#include<unistd.h>
int link(const char *oldpath,const char *newpath);
函数link()主要用于为一个已经存在的文件创建一个新的硬链接。
参数oldpath代表已经存在的文件,参数newpath代表创建的新的硬链接的文件名。这两个文件路径需要在一个文件系统中。如果
newpath文件已经存在,则不会在这个文件中写入数据。
该函数如果调用成功,返回值为0;否则返回值为1,
并设置相应的errno信息。
þ 删除硬链接函数unlink()
系统调用函数unlink()的定义形式如下:
#include<unistd.h>
Int unlink(const char *pathname);
函数unlink()主要用于删除一个已经存在的硬链接文件。参数pathname指向的就是这个存在的硬链接文件的路径名称。
通过系统调用函数link()为已经存在的文件old.c创建了一个硬链接,名称为hardlink.c,并打开这个硬链接文件,打开10秒后,再通过
unlink()函数删除此硬链接文件。程序代码如下:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>z
#include<stdio.h>
#include<stdlib.h>
int main()
{
char *oldpath="/home/cff/9/old.c"; /*原文件路径*/
char *newpath="/home/cff/9/hardlink.c"; /*新硬链接文件路径*/
if(link(oldpath,newpath)==-1) /*创建一个硬联接*/
{
perror("create hard link failed!");
return 1;
}
printf("create hard link successful!\n");
if(open(newpath,O_RDWR)<0) /*打开这个硬链接*/
{
perror("open error!");
return 1;
}
printf("open successful!\n");
sleep(10); /*暂停10秒*/
if(unlink(newpath)<0) /*删除硬链接文件*/
{
perror("unlink error!");
return 1;
}
printf("file unlink!\n");
sleep(10);
printf("well done!\n");
return 0;
}
2.符号链接
符号链接是通过文件名称来指向另一个文件的,因此符号链接文件和原文件的索引节点号并不同,一旦将原文件删除,那么符号链接文件
就会无效。符号链接较硬链接方便很多,可以给任意类型的文件建立符号链接。
在Linux系统下,提供了系统调用函数symlink()和系统调用函数readlink()对符号链接进行创建并操作。
þ 创建符号链接symlink()
函数symlink()主要用于为一个已经存在的文件创建一个符号链接。该函数的定义形式如下:
#include<unistd.h>
int symlink(const char *oldpath,const char *newpath);
参数oldpath指的是原有的文件名称;参数newpath指的是新创建的一个符号链接文件名称。
该函数调用成功返回值为0;否则返回值为1,
并设置相应的errno信息。
?技巧:创建一个新的符号链接文件的函数symlink()与使用link()函数创建一个硬链接的使用方法是相同的,此函数的使用非常简单。
þ 打开符号链接并获取文件名称readlink()
系统调用函数readlink()主要用于打开一个已经存在的符号链接并获取该文件的名称。该函数的定义形式如下:
#include<unistd.h>
ssize_t readlink(const char *path,char *buf,size_t bufsiz);
参数path指的是已经存在的符号链接的路径;参数buf指向一块缓冲区,用于存放读取出来的信息;参数bufsiz指的是该缓冲区的大小。
函数调用成功返回值为实际写入缓冲区的字节数;调用失败时返回值为1,
并设置相应的errno信息。
& 说明:删除符号链接文件的系统调用函数与删除硬链接文件的系统调用函数是相同的,都使用unlink()函数。
============================================================================
                                     文件基于文件描述符的读写、基于数据流的读写
============================================================================
基于文件描述符的I/O操作函数:open()、close()、read()、write()、lseek()等等。
1.open()函数
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);
int creat(const char *pathname,mode_t mode);
上述介绍的两个open()函数和一个creat()函数,都是调用成功时,返回其新分配的文件描述符;否则返回值为1,
并设置适当的errno
值。
参数pathname均代表要打开或者要创建的这个文件的路径名称;
参数flags均代表文件的打开方式的宏定义,这些宏定义的含义如表10.1所示;
而参数mode均代表文件的访问权限设置,访问权限的宏定义的含义如表10.2所示。
表10.1 open函数的flag参数定义
文件打开方式的宏定义 含义
O_RDNOLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以读写方式打开文件
O_CREAT 若所打开文件不存在,则创建该文件
O_EXCL 如果打开文件时设置了O_CREAT,但是该文件存在,则导致调用失败
O_NOCTTY 如果在打开tty时,进程没有控制tty,则不控制终端
O_TRUNC 如果以只写或读写方式打开一个已存在的文件时,将该文件截至0
O_APPEND 以追加的方式打开文件
O_NONBLOCK 用于非堵塞套接口I/O,若操作不能无延迟的完成,则在操作前返回
O_NODELAY 同O_NONBLOCK
O_SYNC 当数据被写入外存或者其他设备之后,操作才返回
文件的打开方式的宏定义可以使用“|”或运算进行组合,但其中必须包括O_RDONLY、O_WRONLY或O_RDWR三种打开方式的宏定义
之一。
文件的权限的宏定义在应用时,也可以使用“|”或运算进行组合使用。
表10.2 文件权限的宏定义
宏定义 八进制值 说明
S_IRWXU 00700 设置文件所有者可读、写和执行的权限
S_IRWXG 00070 设置文件所在用户组的可读、写和执行的权限
S_IRWXO 00007 设置其他用户的可读、写和执行的权限
S_IRUSR 00400 设置文件所有者的读权限
S_IWUSR 00200 设置文件所有者的写权限
S_IXUSR 00100 设置文件所有者的执行权限
S_IRGRP 00040 设置用户组的读权限
S_IWGRP 00020 设置用户组的写权限
S_IXGRP 00010 设置用户组的执行权限
S_IROTH 00004 设置其他用户的读权限
S_IWOTH 00002 设置其他用户的写权限
S_IXOTH 00001 设置其他用户的执行权限
参数mode代表的文件权限,可以用八进制数表示,比如0644表示rwrr,
也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表
示,要注意的是,文件权限由open()函数的mode参数和当前进程的umask掩码共同决定,umask掩码是系统中默认的值,可以通过
在终端下输入命令“umask”查询此值。
2.close()函数
close函数主要用于关闭一个已打开的文件,该函数的定义形式如下:
#include<unistd.h>
Int close(int fd);
close()函数如果调用成功,返回值为0;否则返回值为1,
并设置适当的errno值。
参数fd是要关闭的文件描述符。
注意:当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close()函数关闭,所以即使用户程序不调用close()函数,在终
止时内核也会自动关闭它打开的所有文件。但是对于网络服务器这种一直在运行的程序,文件描述符一定要及时关闭,否则随着打开的文
件越来越多,会占用大量文件描述符和系统资源。
文件的读写操作
提供了系统调用函数read()和write()实现文件的读写操作。
1.read()函数
read函数从打开的文件(包括设备文件)中读取数据。该函数的定义形式如下:
#include<unistd.h>
ssize_t read(int fd,void *buf,size_t count);
参数fd代表的是要进行读写的文件的描述符,参数buf代表的是读取的数据存放在buf指针所指向的缓冲区中,参数count代表的是读取
的数据的字节数。读取文件数据时,文件的当前读写位置会向后移。
U注意:这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用
户空间I/O缓冲区中的位置。
函数调用成功,返回值为读取的字节数,否则返回值为1,
并设置适当的errno值。
返回的字节数有时候会小于参数count值。以下几种读取文件数据情况下,返回的字节数会小于count值。如:
þ 读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回
30,下次read将返回0。
þ 从终端设备读,通常以行为单位,读到换行符就返回了。
þ 从网络读,根据不同的传输层协议和内核缓存机制,返回值可能小于请求的字节数
2.write()函数
write()函数向打开的设备或文件中写入数据,该函数的定义形式如下:
#include<unistd.h>
ssize_t write(int fd,const void *buf,size_t count);
参数fd代表的是想要写入数据的文件的文件描述符,参数buf指向写入文件的数据的缓冲区,参数count代表的是写入文件的数据的字节
数。
函数调用成功时,返回值为写入的字节数,否则返回值为1,
并设置适当的errno值。
& 说明:当向常规文件写入数据时,返回值会是字节数count,但是向终端设备或者网络中写入数据,返回值则不一定为写入的字节
数。
文件的定位
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字
节。
技巧:以O_APPEND方式打开文件,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。
lseek()函数可以移动当前读写位置,通常也称为偏移量。该函数的定义形式如下
#include<sys/types.h>
#include<unistd.h>
off_t lseek(int fildes,off_t offset,int whence);
参数fildes代表的是文件描述符,参数offset代表的是偏移量,参数whence代表的是用于偏移时的相对位置。参数whence可取如下
几个值,代表偏移值的相对位置。
þ SEEK_SET:从文件的开头位置计算偏移量。
þ SEEK_CUR:从当前的位置开始计算偏移量。
þ SEEK_END:从文件的末尾计算偏移量。
偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,未写入内容的空间用‘\0’填满。
该函数如果调用成功,返回值为新的偏移量,否则返回值为1,
并设置适当的errno值。
通过调用上述介绍的几种系统调用函数,对文件进行简单的读写操作。程序代码如下:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main()
{
char *path="/home/cff/9/test.c"; /*进行操作的文件路径*/
int fd;
char buf[40],buf2[]="hello mrcff"; /*自定义读写用的缓冲区*/
int n,i;
if((fd=open(path,O_RDWR))<0) /*打开文件*/
{
perror("open file failed!");
return 1;
}
else
printf("open file successful!\n");
if((n=read(fd,buf,20))<0) /*读取文件中的数据*/
{
perror("read failed!");
return 1;
}
else
{ printf("output read data:\n");
printf("%s\n",buf); /*将读取的数据输出到终端控制台*/
}
if((i=lseek(fd,11,SEEK_SET))<0) /*定位到从文件开头处到第11个字节处*/
{
perror("lseek error!");
return 1;
}
else
{
if(write(fd,buf2,11)<0) /*向文件中写入数据*/
{
perror("write error!");
return 1;
}
else
{
printf("write successful!\n");
}
}
close(fd); /*关闭文件的同时保存了对文件的改动*/
if((fd=open(path,O_RDWR))<0) /*打开文件*/
{
perror("open file failed!");
return 1;
}
if((n=read(fd,buf,40))<0) /*读取数据*/
{
perror("read 2 failed!");
return 1;
}
else
{
printf("read the changed data:\n");
printf("%s\n",buf); /*将数据输出到终端*/
}
if(close(fd)<0) /*关闭文件*/
{
perror("close failed!");
return 1;
}
else
printf("good bye!\n");
return 0;
}
该程序的实现分为如下几步:
(1) 打开文件;
(2) 读取文件中的数据;
(3) 输出到终端控制台上;
(4) 给文件指针定位到指定位置处;
(5) 在定位的位置处,写入指定信息;
(6) 关闭文件,数据保存;
(7) 再次打开此文件;
(8) 读取文件中修改后的数据;
(9) 将数据输出到终端控制台上;
(10) 关闭文件;
————————————————————————————————————————————
基于流的I/O操作函数有:fopen()、fclose()、fread()、fwrite()、fscanf()、fgetc()等等。
1.fopen()函数
打开文件就是在操作系统中分配一些资源用于保存该文件的状态信息,用文件描述符来引用这个文件的状态信息。因此可以通过这个文件
描述符对文件进行某些操作。
函数fopen()的定义形式如下:
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
参数path代表的是要打开的文件的路径名;参数mode指的是文件的打开方式。
函数调用成功时,返回值为文件指针,否则返回NULL并设置errno信息。
返回的这个文件指针主要用于以后调用其它函数对文件做读写操作,该指针用以指明对哪个文件进行操作。
mode参数有如下6种取值,如表10.3所示。
表10.3 文件打开方式取值
字符 功能说明
r 只读,文件必须已存在
r+ 允许读和写,文件必须已存在
w 只写,如果文件不存在则创建,如果文件已存在则把文件长度截断(Truncate)为0字节再重新写,也就是替换掉原来的文件内容
w+ 允许读和写,如果文件不存在则创建,如果文件已存在则把文件长度截断为0字节再重新写
a 只能在文件末尾追加数据,如果文件不存在则创建
a+ 允许读和追加数据,如果文件不存在则创建
2.fclose()函数
fclose()函数关闭文件,即释放文件在操作系统中占用的资源,使文件描述符失效,用户程序就无法操作这个文件了。
函数fclose()的定义形式如下:
#include <stdio.h>
int fclose(FILE *fp);
参数fp是要关闭的文件的指针。
函数如果调用成功,返回值为0,否则返回EOF并设置errno信息。
技巧:EOF在stdio.h中定义的,值为1,
它的定义形式如下:
#ifndef EOF
# define EOF (1)
#endif
字符输入输出;对字符的输入输出操作,其实就是以字节为单位的读写操作。在C标准库中常用的读写字符的函数是fgetc()和fputc()。
1.fgetc()函数
fgetc函数从指定的文件中读一个字节。
#include <stdio.h>
int fgetc(FILE *stream);
参数stream为FILE结构体类型的指针,用于指向一个文件。使得该函数从指定文件中读取一个字节。
函数fgetc()如果调用成功,则返回读到的字节;如果出错或者读到文件末尾时返回EOF。
& 说明:在程序中,偶尔会遇到getchar()函数,也是用于读取一个字节,但是它为从标准输入读一个字节。在程序中调用
getchar()函数相当于调用fgetc(stdin)。
注意:在使用fgetc()函数时需要注意以下几点:
(1)调用fgetc()函数时,指定的文件的打开方式必须是可读的。
(2)函数fgetc()调用成功时,返回的是读到的字节,应该为unsigned char类型,但fgetc()函数原型中返回值类型是int,原因在于
函数调用出错或读到文件末尾时fgetc()会返回EOF,即1,
保存在int型的返回值中是0xffffffff,如果读到字节0xff,由unsigned
char型转换为int型是0x000000ff,只有规定返回值是int型才能把这两种情况区分开,如果规定返回值是unsigned char型,那么当
返回值是0xff时无法区分到底是EOF还是字节0xff。
2.fputc()函数
fputc()函数主要用于向指定的文件写一个字节,该函数的定义形式如下:
#include <stdio.h>
int fputc(int c, FILE *stream);
该函数可以理解为将字节c写入到stream指针所指向的文件中。
函数如果调用成功,则返回写入的字节,否则,返回EOF。
说明:在程序中,偶尔会遇到putchar()函数,也是用于向文件中写入一个字节,但是它为向标准输出写一个字节。在程序中调用
putchar()函数相当于调用fputc(c,stdout)。
注意:在使用fputc()函数时需要注意,调用fputc()函数时,指定文件的打开方式必须是可写的(包括追加)。
3.字符I/O的实例
此实例主要实现了多次调用fputc()函数向文件test.c中写入数组a中的字节,然后通过多次调用fgetc()函数获取文件中的数据存放在字
符变量ch中,将其显示到终端屏幕上。
#include<stdio.h>
int main()
{
FILE *fp;
int i;
char *path="/home/cff/10/test.c";
char a[]={'h','e','l','l','o',' ','m','r'};
char ch;
fp=fopen(path,"w"); /*以只写打开文件*/
if(fp) /*判断是否成功打开文件*/
{
for(i=0;i<5;i++)
{
if(fputc(a[i],fp)==EOF) /*向文件中循环写入a数组中的内容*/
{
perror("write error!");
return 1;
}
}
printf("write successful!\n");
}
else
{
printf("open error!\n");
return 1;
}
fclose(fp); /*关闭文件*/
if((fp=fopen("/home/cff/10/test.c","r"))==NULL) /*以只读的形式打开文件*/
{
perror("open error!");
return 1;
}
printf("output data in the test.c\n");
for(i=0;i<5;i++)
{
if((ch=fgetc(fp))==EOF) /*循环方式获取文件中的5个字节*/
{
perror("fgetc error!");
return 1;
}
else
{
printf("%c",ch); /*输出字符*/
}
}
printf("\nget successful!\nplease examine test.c...\n");
fclose(fp);/*关闭文件*/
return 0;
}
字符串输入输出:C标准库函数为字符串的输入输出提供了fputs()函数和fgets()函数。fputs()函数与fputc()函数类似,不同的是
fputc()每次只向文件中写一个字符,而fputs()函数每次向文件中写入一个字符串。Fgets()函数与fgetc()函数之间的关系是读取字符
串与读取字符的关系。
1.fgets()函数
函数fgets()的定义形式如下:
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
该函数实现了从参数stream所指向的文件中读取一串小于参数size所表示的字节数的字符串;然后将字符串存储到s所指向的缓冲区
中。
函数调用成功时返回内容为返回指向s所指向的缓冲区部分的指针;函数调用出错或者读到文件末尾时返回NULL。
在调用fgets()函数读取字符串时,以读取到‘\n’转义字符为结束,并在该行末尾添加一个'\0'组成完整的字符串。
在size字节范围内没有读到‘\n’结束符,则添加一个'\0',组成字符串存储到缓冲区中,文件中剩余的字符,等待下次调用fgets()函数时
再读取。
注意:对于fgets而言,‘\n’是一个特别的字符,作为结束符;而‘\0’并无任何特别之处,只用作普通字符读入。正因为‘\0’作为一个普通的
字符,因此无法判断缓冲区中的'\0'究竟是从文件读上来的字符还是由fgets()函数自动添加的结束符,所以fgets()函数只用于读文本文
件而不提倡读二进制文件,并且文本文件中的所有字符不能有'\0'。
2.fputs()函数
函数fputs()的定义形式如下:
#include <stdio.h>
int fputs(const char *s, FILE *stream);
此函数用于实现向stream指针指向的文件写入s缓冲区中的字符串。
函数如果调用成功返回值为一个非负整数,否则返回EOF。
& 说明:缓冲区s中保存的是以'\0'结尾的字符串,fputs将该字符串写入文件stream,但并不写入结尾的‘\0’,且字符串中可以
有'\n'也可以没有‘\n’。
数据块输入输出:在C标准库函数中,用于对文件的数据块的输入与输出的函数为fread()和fwrite()。数据块输入输出又被称为直接输入
输出和以记录的形式输入输出。
这两个函数的定义形式如下:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
函数fread()和函数fwrite()用于读写数据块,参数size指出一个数据块的大小,而nmemb指出要读或写多少条这样的数据块,这些数
据块在ptr指针所指的内存空间中连续存放,共占(size*nmemb)个字节。
函数fread()从stream所指的文件中读出(size*nmemb)个字节保存到ptr中,而函数fwrite()把ptr中的(size*nmemb)个字节写
到stream所指定的文件中。
函数fread()和函数fwrite()调用成功,则返回读或写的记录数,该记录数等于nmemb,如果函数调用出错或读到文件末尾时返回的记
录数小于nmemb,也可能返回0。
格式化输入输出:所谓的格式化输入输出就是按照一定的格式将数据进行输入输出操作。在程序中经常用到的printf()函数和scanf()函数
是用于队终端设备文件的读写操作,这两个函数被称之为格式化输入输出,因为在使用这两个函数时,需要指定读写数据的数据类型,按
照一定的格式进行读写,如“%d”或者“%c”等等。
接下来先介绍一下stdio库中提供的格式化操作的输出函数。
1.格式化输出函数
在Linux系统的man命令中,查找到格式化输出的函数有如下4种定义形式:
#include <stdio.h>
int printf(const char *format,…);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
这几个函数调用成功时,返回格式化输出的字节数(不包括字符串的结尾‘\0’),如果出错,返回一个负值。
þ printf()函数中参数format代表一个格式化的输出字符串,该函数主要实现了向标准输出流中输出。
þ fprintf()函数用于向stream指针所指向的文件中输出format所代表的数据。
þ sprintf()函数与snprintf()函数都不是用于向流中输出数据。而是向str所代表的字符串中输出数据。而snprintf()函数中多了一个
参数size,用于为str所指的缓冲区设置一个大小。这样不容易出现缓冲区溢出的问题。
& 说明:这几个函数中的参数format均代表格式化的输出字符串,可以通过命令“man printf”,查看这几个格式化输出函数以及这
些格式控制符的详细内容。
2.格式化输入函数
介绍完格式化输出函数,现在介绍一下格式化输入函数,在Linux系统的man命令中,查找到格式化输入函数有如下3种定义形式,与
printf()等格式化输出函数相互对应,但由于从缓冲区中读取字符串不会发生缓冲区溢出的问题,因此不存在与snprintf()函数相互对应
的格式化输入函数。这3个格式化输入函数之间的区别与前面介绍到的几个格式化输出函数之间的区别是相同的。
这3个格式化输入函数的定义形式如下:
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);
参数format也表示格式化的字符串,只是在这里是代表输入的字符串。
函数scanf()从标准输入获取格式化字符串,在函数中除了有格式化字符串之外,还有一个参数的地址,用于将输入的字符串传给这个地
址。fscanf从指定的文件stream中获取字符,而sscanf从指定的字符串str中读字符。
这几个格式化输入函数如果调用成功,则返回成功匹配和赋值的参数个数,成功匹配的参数可能少于所提供的赋值参数,返回0表示一个
都不匹配,出错或者读到文件或字符串末尾时返回EOF并设置errno
操作读写位置的函数:对文件中的数据进行读写操作时,往往并不需要从头开始读写,只需对其中指定内容进行操作,这时就需要使用文
件定位函数来实现对文件的随机读取,本节中将介绍三种文件定位函数。
三种文件定位函数的定义形式如下:
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
1.fseek()函数
函数fseek()的作用是用来移动文件内部位置指针。其中,stream指向被移动的文件;offset表示移动的字节数,要求位移量是long型
数据,以便在文件长度大于64KB时不会出错。当用常量表示位移量时,要求加后缀“L”;参数whence表示从何处开始计算位移量,规
定的起始点有三种:文件首、文件当前位置和文件尾。其表示方法如表10.4所示。
表10.4 参数whence的取值
起始点 符号表示 数字表示
文件首 SEEK_SET 0
当前位置 SEEK_CUR 1
文件结尾 SEEK_END 2
例如:
fseek(fp,20L,
1);
表示将读写位置指针从当前位置向后退20个字节。
说明:fseek函数一般用于二进制文件。在文本文件中由于要进行转换,往往计算的位置会出现错误。
函数fseek()如果调用成功,返回值为0;否则返回值为1,
并设置适当的errno值。
此实例只是简单的实现了在一个文件file.c中的第二个字节处插入字符“m”。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
if ( (fp = fopen("file.c","r+")) == NULL) /*以读写的方式打开一个已存在的文件file.c*/
{
perror("Open file textfile");
exit(1);
}
if (fseek(fp, 2, SEEK_SET) != 0) /*将读写位置定位在从文件开头处计算的第2个字节处*/
{
perror("Seek file textfile");
exit(1);
fputc('m', fp); /*在此处插入字符m*/
fclose(fp); /*关闭文件*/
return 0;
}
注意:fseek(fp,2, SEEK_SET)将读写位置移到第2个字节处(其实是第3个字节,从0开始计数),然后在该位置写入一个字符m。
2.ftell()函数
函数ftell()的作用是得到stream指定的流式文件中的当前位置,用相对于文件开头的位移量来表示。
函数如果调用成功,返回值为当前的位移量,否则返回值为1,
并设置适当的errno值。
可以调用如下代码获取文件的字符串长度,如:
n=ftell(fp);
3.rewind()函数
函数rewind()的作用是使位置指针重新返回文件的开头,该函数没有返回值。
此实例通过读取一个文件中的字符,了解ftell()函数是如何获取当前位置处的位移量的,并且还了解了rewind()函数是如何将位置指针定
位到文件的开头位置,程序的代码如下:
#include<stdio.h>
#include<stdlib.h>
main()
{
FILE *fp;
char ch,filename[50];
printf("请输入文件路径及名称:\n");
scanf("%s",filename); /*输入文件名*/
if((fp=fopen(filename,"r"))==NULL) /*以只读方式打开该文件*/
{
printf("不能打开的文件!\n");
exit(0);
}
printf("len0=%d\n",ftell(fp)); /*输出当前位置*/
ch = fgetc(fp);
while (ch != EOF)
{
putchar(ch); /*输出字符*/
ch = fgetc(fp); /*获取fp指向文件中的字符*/
}
printf("\n");
printf("len1=%d\n",ftell(fp)); /*输出位置指针的当前位置*/
rewind(fp); /*指针指向文件开头*/
printf("len2=%d\n",ftell(fp)); /*输出位置指针当前位置*/
ch = fgetc(fp);
while (ch != EOF)
{
putchar(ch); /*输出字符*/
ch = fgetc(fp);
}
printf("\n");
fclose(fp); /*关闭文件*/
}
在上述程序中,主要实现了如下几点功能:
(1)成功打开文件后,输出当前位置指针的位移量len0;
(2)然后读取文件中的字节,当读取完文件中的字节,输出此时位置指针的位移量len1;
(3)接着,调用rewind()函数将位置指针定位到文件的开头,再次输出当前的位移量len2。
(4)最后,关闭文件。
C标准库的I/O缓冲区
C标准库在调用fopen()函数时,都会给此文件分配一个I/O缓冲区,可以加速读写操作,原因在于用户程序需要调用C标准I/O库函数(如
fread()、fwrite()等基于流的I/O操作)读写文件,当缓冲区装满后,再由系统调用的I/O函数(如read()、write()等基于文件描述符的
I/O操作)把读写请求传给内核,最终由内核驱动磁盘或设备完成I/O操作。
由此看来,为文件分配的内存缓冲区的大小,直接影响到实际操作外存设备的次数,内存中为文件分配的缓冲区越大,操作外存的次数会
越小,因此,读写数据的速度会越来越快,效率就会随之增高。
然而,有时候用户程序等不及将缓冲区都装满之后在传给内核,进行I/O操作,希望把I/O缓冲区中的数据立刻传给内核,让内核写回设
备,这种行为叫做flush操作,对应的库函数是fflush()。通常在fclose()函数在关闭文件之前也会做flush操作。
C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。
(1)全缓冲
如果缓冲区写满了就写回内核。普通文件通常是全缓冲的。
(2)行缓冲
如果用户程序写的数据中有“\n”,就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是
行缓冲的。
(3)无缓冲
用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出
到设备。
使用缓冲区时,会使用到如下两类操作,一是设置缓冲区属性;二是清空缓冲区。
1.设置缓冲区属性
缓冲区的大小直接影响到程序的执行效率,因此缓冲区的属性设置很重要,缓冲区的属性主要包括缓冲区的类型和其大小。通常都是系统
的默认值。然而在实际应用时,也可以通过系统提供的一些调用函数修改缓冲区的属性,这几个函数的定义形式如下:
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);
void setlinebuf(FILE *stream);
int setvbuf(FILE *stream, char *buf, int mode , size_t size);
U注意:这几个函数只针对流的属性而设定,而且在使用上述函数时,流还必须是打开的。
(1) 函数setbuf()主要实现了为参数buf所指定的缓冲区设定大小,此函数中,设定缓冲区大小的值只有两个,一个是常数
BUFSIZ,另一个是NULL。当定义值为BUFSIZ时,代表设置缓冲区为全缓冲,若为NULL,代表设置缓冲区为无缓冲形式。
(2) 函数setbuffer()与函数setbuf()功能相同,只是setbuffer()函数可以任意指定缓冲区大小为size。
(3) 函数setlinebuf()实现了将stream指向的缓冲区设定为行缓冲。
(4) 函数setvbuf()融合了上述三中函数的功能,既可以设置缓冲区任意大小size,也可以设置缓冲区的任意类型,如,mode参
数取值为_IOFBF(全缓冲类型),_IOLBT(行缓冲类型),_IONBF(无缓冲类型)。
当设置缓冲区属性的函数调用成功时,返回值为0,否则返回值为非0。
2.清空缓冲区
在本节的前面部分,已经介绍到关于flush操作。flush操作可以将I/O缓冲区中的内容强制保存到磁盘文件中,使得缓冲区清空。
在stdio库中提供了fflush()函数实现此功能,该函数的定义形式如下:
#include <stdio.h>
int fflush(FILE *stream);
函数fflush()就实现将缓冲区中的尚未写入文件的数据,强制性的写进stream所指的文件中,然后清空缓冲区。
如果stream为NULL,此函数会将所有打开的文件数据更新。
============================================================================
                                                                UDP&TCP网络编程函数
============================================================================
1.套接字建立
为了建立套接字,程序可以调用socket()函数,该函数返回一个类似于文件描述符的句柄。socket()函数原型为:
int socket(int domain, int type, int protocol);
参数domain代表所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);参数type指定套接字的类型,type可
取值为:SOCK_STREAM(流式套接字) 或SOCK_DGRAM(数据包套接字),socket接口还定义了SOCK_RAW(原始套接
字),允许程序使用底层协议;参数protocol通常赋值"0"。
2.套接字配置
通过socket()函数调用返回一个套接字描述符后,在使用套接字进行网络传输以前,必须配置该套接字。面向连接的套接字客户端通过调
用connect()函数在套接字数据结构中保存本地信息和远端信息。无连接套接字的客户端和服务端以及面向连接套接字的服务端通过调用
bind()函数来配置本地信息。
bind()函数将套接字与本机上的一个端口相关联,随后就可以在该端口下,监听服务请求。bind()函数的定义形式如下:
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
参数sockfd是调用socket()函数返回的套接字描述符;参数my_addr是一个指向包含有本机IP地址及端口号等信息的sockaddr类型
的指针;参数addrlen通常被设置为结构体struct sockaddr的长度,即sizeof(struct sockaddr)。
3.字节优先顺序
计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。在互联网上数据以高位字节优先顺序在网络上传输,所以对于在内
部是以低位字节优先方式存储数据的机器,在互联网上传输数据时就需要进行转换,否则就会出现数据不一致。
4.连接建立
面向连接的客户程序使用connect()函数来配置套接字并与远端服务器建立一个TCP连接,该函数的定义形式如下:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
参数sockfd 是socket()函数返回的套接字描述符;参数serv_addr是包含远端主机IP地址和端口号的指针;参数addrlen是远端地质
结构的长度。
5.监听模式
函数listen()使套接字处于被动的监听模式,并为该套接字建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序对它们
进行处理它们。
int listen(int sockfd, int backlog);
参数sockfd 是socket()函数返回的套接字描述符;参数backlog指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等
待accept()等系统调用的操作。参数backlog对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为20。如果一个服务请求
到来时,输入队列已满,该套接字将拒绝连接请求,客户将收到一个出错信息。
6.接收请求
accept()函数让服务器接收客户的连接请求。在建立好输入队列后,服务器就调用accept()函数,然后睡眠并等待客户的连接请求int
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数sockfd 是socket()函数返回的套接字描述符;参数addr通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求
服务的主机的信息;参数addrten通常为一个指向值为sizeof(struct sockaddr_in)的整型指针变量。
函数调用出错时accept()函数返回1,
并置相应的errno值。
7.数据传输
函数send()和recv()用于在面向连接的套接字上进行数据传输。
ssize_t send(int sockfd, const void *msg, size_t len, int flags);
参数sockfd 是socket()函数返回的套接字描述符;参数msg是一个指向要发送数据的指针;参数len是以字节为单位的数据的长度;参
数flags一般情况下置为0。
send()函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序中应该将send()的返回值与想要发送的字节数进行比
较。当send()返回值与len不匹配时,应该对这种情况进行处理。
ssize_t recv(int s, void *buf, size_t len, int flags);
参数sockfd 是socket()函数返回的套接字描述符;参数buf 是存放接收数据的缓冲区;参数len是缓冲的长度。参数flags也被置为0。
函数recv()返回实际上接收的字节数,当出现错误时,返回1
并置相应的errno值。
函数sendto()和recvfrom()用于在无连接的数据报套接字方式下进行数据传输。由于本地套接字并没有与远端机器建立连接,所以在发
送数据时应指明目的地址。
8.结束传输
数据操作结束后,就可以调用close()函数来释放该套接字,从而停止在该套接字上的任何数据操作,该函数的定义形式如下:
int close(int fd);
int shutdown(int s, int how);
参数sockfd是需要关闭的套接字的描述符;参数how允许为关闭操作选择以下几种方式:
0:不允许继续接收数据
1:不允许继续发送数据
2:不允许继续发送和接收数据
TCP
UDP
UDP
是面向无连接的网络通信,并不需要像TCP套接字编程那样需要通过connect()与服务器建立连接,然后调用
listen()函数使服务器处于监听状态,在通过accept()函数接收客户端的连接请求。UDP套接字编程,只需要创建用于
通信的套接字,然后在服务器端绑定端口,然后就可以实现数据的传输。
在绑定了地址信息之后,进行数据传输时,服务器会阻塞recvfrom()函数,等待客户端调用sendto()函数发送数据,
同时客户端的recvfrom()被阻塞,然后,服务器会调用recvfrom()函数接收数据,接着向客户端作出应答,同时,服
务端的recvfrom()被阻塞,接着,客户端调用recvfrom()接收数据。数据传输结束时,需要调用close()函数结束套
接字。
UDP协议的这种C/S机制的通信原理可以理解为如图所示。
基于UDP的网络编程中,主要用到的函数有socket()、bind()、sendto()、recvfrom()和close()。
在前面的套接字编程原理一节中,已经对创建套接字函数socket()、绑定套接字函数bind()和关闭套接字函数close()
进行了介绍,在此对用于无连接的数据报套接字方式下的数据传输的函数sendto()和函数recvfrom()进行介绍。
U注意:sendto()函数和recvfrom()函数可用于面向连接的或无连接的套接字通信中。
1.发送数据
函数sendto()用于向指明目的地址的远端机器发送数据。该函数的定义形式如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t
tolen);
参数s代表套接字描述符,参数buf用于指向发送信息的缓冲区的指针,参数len是发送的信息的长度,参数flags通常会
设置为0,代表的是相关控制参数,主要用于控制是否接收数据以及是否预览报文。参数to为存放接收处的信息的指针,
参数tolen是接收方地址的大小。
函数如果调用成功,返回值为发送的字节数,否则返回值为1,
并设置相应的errno值。
说明:如果sendto()函数用于面向连接的网络通信时,套接字类型为SOCK_STREAM或SOCK_SEQPACKET。此
时参数to指向NULL,参数tolen为0,若不为此值,就会出现错误信息提示。
2.接收数据
函数recvfrom()实现了接收消息。该函数的定义形式如下:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
参数s为套接字描述符,参数buf指向接收信息的指针,参数len代表缓冲区的最大长度,参数flag通常设置为0,表示相
关控制参数,参数from表示发送此信息处的地址指针,参数fromlen指向发送处地址大小的指针。

linux 编程函数原型与用法相关推荐

  1. Linux编程下open()函数的用法

    Linux编程下open()函数的用法 open(打开文件)  相关函数:  read,write,fcntl,close,link,stat,umask,unlink,fopen 表头文件 : #i ...

  2. linux网络编程函数解析之——setsockopt / getsockopt用法

    linux网络编程函数解析之--setsockopt / getsockopt用法 工程中无线传输方面的东西用到了setsockopt(),getsockopt().网上相关博客很多,而且类似,原文出 ...

  3. linux编程取消wait函数,Linux编程基础之进程等待(wait()函数).pdf

    Linux编程基础之进程等待(wait()函数) 编程过程中,有时需要让一个进程等待另一个进程 ,最常见的是父进程等待自己的子进程 ,或者父进程回收自己 的子进程资源包括僵尸进程.这里简单介绍一下系统 ...

  4. 【Linux编程】零拷贝之splice( )函数和tee( )函数

    关于零拷贝技术的相关文章,请参考:[Linux编程]大冒险之零拷贝技术探究 splice( )函数 在两个文件描述符之间移动数据,同sendfile( )函数一样,也是零拷贝. 函数原型: #incl ...

  5. linux 编程--prctl()函数应用

    int prctl ( int option,unsigned long arg2,unsigned long arg3,unsigned long arg4,unsigned long arg5 ) ...

  6. sqrt函数原型c语言,C语言sqrt函数的实例用法讲解

    前言继承是OOP设计中的重要概念.在C++语言中,派生类继承基类有三种继承方式:私有继承(private).保护继承(protected)和公有继承(public).一.继承规则继承是C++中的重要特 ...

  7. linux编程下signal()函数

    首先说明函数指针的定义形式:      <存储类型> 数据类型 (* 函数指针名) (参数表):     其中存储类型一般不写,用默认形式.可以选auto型.static型和extern型 ...

  8. m行n列最大值和最小值C语言,找数组最值 按如下函数原型编程从键盘输入一个m行n列的二维数...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 找数组最值 按如下函数原型编程从键盘输入一个m行n列的二维数组,然后计算数组中元素的最大值及其所在的行列下标值.其中,m和n的值由用户键盘输入.已知m和n ...

  9. Linux 编程中的API函数和系统调用的关系【转】

    转自:http://blog.chinaunix.net/uid-25968088-id-3426027.html 原文地址:Linux 编程中的API函数和系统调用的关系 作者:up哥小号 API: ...

最新文章

  1. 左神讲算法——异或的高级操作(两数交换+经典面试题)
  2. consolez设置
  3. IPv6新形势下的安全解决方案
  4. 棋牌游戏服务器架构: 详细设计(三) 数据库设计
  5. 不同网段虚拟机,互相访问时的路由配置,附路由知识学习
  6. Windows下Android开发环境 搭建
  7. ie9服务器win2008系统离线安装包,IE9离线安装包完整版
  8. 《算法导论》:关于循环不变式
  9. QEMU同步脏页原理
  10. 好斌c语言教程,C语言学习大纲__斌(讲解).doc.doc
  11. 《动手学深度学习》(PyTorch版)代码注释 - 47 【Image_augmentation】
  12. Java的在哪里找labor_LaborDay哪里玩
  13. 拉升股价套利为饵狂骗157万 广州邦臣机械气化炉警方:防理财诈骗
  14. SSM 电影后台管理项目
  15. tws耳机哪个品牌好?国产好用的tws耳机推荐
  16. Oracle的安装及导入.dmp文件教程
  17. 服务器名称没有显示,远程服务器名称问题没有解决
  18. 中国新四大发明诞生 主打吃逛买以互联网为主
  19. MySQL查询 json 字段中是否包含某个value
  20. js实现城市首字母导航

热门文章

  1. 自建Alist共享网盘网站
  2. MPS DC-DC Designer帮你搞定DC-DC电路设计
  3. 外贸公司一般用什么邮箱,电子邮件如何群发?
  4. nanodet-plus
  5. 十岁不愁、二十不悔、三十而立、四十不惑、五十知天命、六十耳顺、七十古来稀...
  6. [创业] 美国互联网广告07年总开支255亿美元, 增长27%
  7. 绩效考核管理系统使用说明1
  8. 【SpringBoot】SpringBoot三层开发
  9. 程序员成就技术大拿之路
  10. 数字图像处理(15): 灰度直方图(matplotlib 和OpenCV 绘制直方图)