目录
一、进程间通信(IPC)
二、信号(Signal)
1、基本概念
2、进程对信号的处理
(1)signal()函数
(2)sigaction()函数
3、实战演练
三、管道(pipe)
1、基本概念
2、管道的局限性
3、管道的创建
4、实战演练
四、命名管道(fifo)
1、基本概念
2、命名管道的创建
3、实战演练
五、命名socket
1、基本概念
2、命名socket特点
3、命名socket编程
4、实战演练
5、access()函数
在计算机编程和操作系统中,进程间通信(Inter-Process Communication,IPC)是实现不同进程之间数据传输和共享资源的关键技术。在多任务和多进程系统中,各个进程可能需要相互通信以协调任务、共享数据或进行同步操作。本文将深入探讨几种常见的进程间通信方式,如下图所示:
本篇文章主要记载了我学习进程间通信的四种常用方法:信号、管道、命名管道、命名socket。其余的三种方法信号量、共享内存、消息队列我会在下一篇文章中进行详细讲解!
信号是Linux系统中用于进程之间通信或者操作的机制,它给进程提供一种异步的软件中断(信号可以在任何时候发送给某一进程,而无须知道该进程的状态)。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
进程接受到信号后,可以选择忽略、捕捉和默认/缺省三种处理方式。
Linux提供了几十种信号,分别代表着不同的意义。内核针对每个信号,都给其定义了一个唯一的整数编号,从数字 1 开始顺序展开。并且每一个信号都有其对应的名字(其实就是一个宏), 信号名字与信号编号乃是一一对应关系,但是由于每个信号的实际编号随着系统的不同可能会不一样,所以在程序当中一般都使用信号的符号名(也就是宏定义)。这些信号在头文件中定义,每个信号都是以 SIGxxx 为开头。
对信号不是很熟悉的同学,推荐看这篇文章---《APUE学习之信号Signal》,里面关于信号讲的十分详细!
Linux下有signal()和sigaction()两种信号安装的函数,让我们分别来看看:
函数原型如下:
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数描述:
signal函数用来在进程中指定当一个信号到达进程后该做什么处理,信号处理函数的handler有两个默认值,分别是SIG_IGN表示忽略行为和SIG_DFL表示默认行为。而且signal函数是阻塞的,比如当进程正在执行SIGUSR1信号的处理函数,此时又来一个SIGUSR1信号,signal会等到当前信号处理函数处理完后才继续处理后来的SIGUSR1。
函数原型如下:
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
stuct sigaction
{
void (*)(int) sa_handle; //信号处理函数
sigset_t sa_mask; //信号屏蔽集
int sa_flags;
}
参数说明:
(1)第一个参数 signum:信号值。
(2)第二个参数act:信号的处理参数。
(3)第三个参数oldact:保存信号上次安装时的处理参数。
补充:
(1)信号阻塞:和signal函数类似,当正处于某个信号的处理函数中时,这个信号再次到达会被阻塞,待信号处理函数完成之后再处理。
(2)sa_mask:信号屏蔽集,所谓屏蔽并不是忽略,屏蔽的时间段是在信号处理函数执行期间,一旦处理函数执行完毕将会重新唤醒此信号。
(3)sa_flag:通常取值为0,则表示默认行为。
题目:
我们知道,父进程在创建子进程之后,究竟是父进程还是子进程先运行没有规定,这由操作系统的进程调度策略决定,而如果在某些情况下我们需要确保父子进程运行的先后顺序,则可以使用信号来实现进程间的同步。
要求:写一个程序,实现父子进程之间使用信号进行同步。如果父进程先执行则进入到循环休眠等待状态,直到子进程给他发送信号之后才能跳出循环继续运行,确保子进程先执行它的任务。同样,子进程在执行完毕之后,就等待父进程给他发送信号之后才能退出,而父进程则通过调用wait()系统调用等待子进程退出后,父进程再退出。
参考代码如下:
#include
#include
#include
#include
#include
#include
#include
int g_child_stop = 0;
int g_parent_run = 0;
void sig_child(int signum)
{
if(SIGUSR1 == signum)
{
g_child_stop = 1;
}
}
void sig_parent(int signum)
{
if(SIGUSR2 == signum)
{
g_parent_run = 1;
}
}
int main(int argc,char *argv[])
{
int pid;
int status;
signal(SIGUSR1,sig_child);
signal(SIGUSR2,sig_parent);
if((pid=fork()) < 0)
{
printf("Create child process failure:%s\n",strerror(errno));
return -1;
}
else if(pid == 0)
{
/*Child process can do something first here.*/
printf("Child process start runing!\n");
/*when child process have done,then tell parent process to start running*/
printf("Child process send parent a signal to tell parent process to run!\n");
kill(getppid(),SIGUSR2);
/*Waiting the stopping signal sent by parent process*/
while( !g_child_stop )
{
sleep(1);
}
/*Child process have received the stopping signal*/
printf("child process receive signal from parent and exit now!\n");
return 0;
}
/*Only parent process run the codes beneath*/
/*Parents hangs up until receive signal from child*/
while( !g_parent_run )
{
sleep(1);
}
/*Parent process have received the running signal from child process*/
/*Parent process can do something here*/
printf("Parent start running now!\n");
/*Parent process send a signal to tell child process to exit*/
kill(pid,SIGUSR1);
/*parent wait child process exit*/
wait(&status);
printf("Parent wait child process die and exit now!\n");
return 0;
}
运行结果:
管道是UNIX系统IPC的最古老的形式,是最简单、效率最差的一种通信方式。 它的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据。
管道由两个文件描述符引用,一个表示读端,一个表示写端。管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺利地读取数据。
该缓冲区可以看成一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,数据被读出后,就不会再存在于管道之中了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程是否进入等待队列,当空的缓冲区有新数据写入或者原来的缓冲区有数据读出时,就会唤醒等待队列中的进程继续读写。
对管道(pipe)不是很熟悉的同学推荐看这篇文章----《APUE学习之管道pipe》,这篇文章关于管道讲解的十分详细!
(1)半双工通信方式,数据只能在一个方向流动。
(2)管道只能在具有公共祖先之间的两个进程之间使用。
(3)数据一旦被读走,便不在管道中存在,不可反复读取。
(4)管道的缓冲区是有限的(管道存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
(5) 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须实现约定好数据的格式,比如多少字节算一个消息(或命令、或记录)等待;
管道是通过调用pipe函数创建的,让我们先看一下函数原型吧:
#include
int pipe(int pipefd[2]); //返回值:成功返回0,出错返回-1
经由参数fd返回的两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
那创建管道,我们又如何能够实现进程之间通信呢?
通常,我们的进程先调用pipe,接着调用fork,从而创建了父进程与子进程之间的IPC通道。调用fork之后,如图所示:
fork()之后做什么取决于我们想要的数据流的反向,共两种情况:
(1)从父进程到子进程方向的管道
即父写子读,关闭父进程管道的读端fd[0],再关闭子进程管道的写端fd[1]。
(2)从子进程到父进程方向的管道
即子写父读,关闭父进程管道的写端fd[1],再关闭子进程管道的读端fd[0]。
题目:
编写一个程序,实现父进程给子进程方向发送数据。
思考过程:
首先父进程调用pipe创建管道之后fork(),这时子进程会继承父进程所有打开的文件描述符(包括管道),这时对于一个管道就有四个读写端(父子进程各有一对管道读写端),如果需要父进程往子进程里写数据,则需要在父进程中关闭读端,在子进程中关闭写端;而如果需要子进程往父进程中写数据,则可以在父进程关闭写端,然后子进程中关闭读端。
代码如下:
#include
#include
#include
#include
#include
#include
#define MSG_STR "This message is from parent:Hello,child process!"
int main(int argc,char *agv[])
{
int pipe_fd[2];
int rv;
int pid;
char buf[512];
int status;
if(pipe(pipe_fd) < 0)
{
printf("Create pipe failure:%s\n",strerror(errno));
return -1;
}
if((pid = fork()) < 0)
{
printf("Create child prcess failure:%s\n",strerror(errno));
return -2;
}
else if(pid == 0)
{
/*child process close write ,then read data from parent process*/
close(pipe_fd[1]);
memset(buf,0,sizeof(buf));
rv = read(pipe_fd[0],buf,sizeof(buf));
if(rv < 0)
{
printf("Child process read from pipe failure:%s\n",strerror(errno));
return -3;
}
printf("Child process read %d bytes data from pipe:[%s]\n",rv,buf);
return 0;
}
/*parent process close read,then write data to child process*/
close(pipe_fd[0]);
if(write(pipe_fd[1],MSG_STR,strlen(MSG_STR)) < 0)
{
printf("Parent process write data to pipe failure:%s\n",strerror(errno));
return -4;
}
printf("Parent start wait child process exit...\n");
wait(&status);
return 0;
}
运行结果:
命名管道通信也属于进程间通信IPC的一种常见方式,他与管道的不同是:命名管道可以实现两个毫不相干的独立进程间通信。
FIFO不同于管道之处在于它提供一个路径与之相关联,以FIFO的文件形式存在于系统中。它在磁盘上有对应的节点,但是没有数据块。换句话说就是,命名管道只是拥有一个名字和相应的访问权限,并且命名管道的大小为0 ,命名管道就是一个内存文件。
大家不用把命名管道想的很复杂,你就可以把它理解为在文件系统中挂了个名字,但也仅仅是个挂个名而已,没有实际的大小。目的就是能叫进程看到这个文件,因为文件系统中的文件都可以被进程看见。
对命名管道(fifo)不是很熟悉的同学推荐看这篇文章---《APUE学习之命名管道fifo》,这篇文章关于命名管道讲解的十分详细!
我们可以通过调用mkfifo()来创建命名管道。一旦建立,任何进程都可以通过文件名将其打开和进行书写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍在。
让我们先来看一下mkfifo()函数的原型吧:
#include
#includeint mkfifo(const char *pathname, mode_t mode);
参数说明:
(1)第一个参数pathname: 在这里既可以传绝对路径,也可以传相对路径(关于绝对路径与相对路径)。当然,绝对路径更灵活,但是也更长。
(2)第二个参数mode:其实就是创建命名管道时的初始权限,实际权限需要经过umask掩码进行计算。
题目---实现进程间聊天:
在程序中创建两个掩藏的命名管道文件(.fifo_chat1和.fifo_chat2)在不同的进程间进行通信。该程序需要运行两次(即两个进程),其中进程0(mode=0)从标准输入里读入数据后通过命名管道2(.fifo_chat2)写入数据给进程1(mode=1);而进程1(mode=1)则从标准输入里读入数据后通过命名管道1(.fifo_chat1)写给进程0。
请利用命名管道编写一个程序来实现上述要求。
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_FILE1 ".fifo_chat1"
#define FIFO_FILE2 ".fifo_chat2"
int g_stop = 0;
void sig_pipe(int signum)
{
if(SIGPIPE == signum)
{
printf("get pipe broken signal and let programe exit\n");
g_stop = 1;
}
}
int main(int argc,char *argv[])
{
int fdr_fifo;
int fdw_fifo;
int rv;
fd_set rdset;
char buf[1024];
int mode = -1;
if(argc != 2)
{
printf("Usage:%s [0/1]\n",basename(argv[0]));
printf("This chat program need run twice,1st time run with [0] and 2nd time with [1]\n");
return -1;
}
mode = atoi(argv[1]);
/*管道是一种半双工的通信方式,如果要实现两个进程间的双向通信则需要两个管道,即两个管道分别作为两个进程的读端和写端*/
/*测试文件是否存在,不存在则建立*/
if(access(FIFO_FILE1,F_OK))
{
printf("FIFO file \"%s\" not exist and create it now\n",FIFO_FILE1);
mkfifo(FIFO_FILE1,0666);
}
if(access(FIFO_FILE2,F_OK))
{
printf("FIFO file \"%s\" not exist and create it now\n",FIFO_FILE2);
mkfifo(FIFO_FILE2,0666);
}
signal(SIGPIPE,sig_pipe);
if(0 == mode)
{
/*这里以只读模式打开命名管道FIFO_FILE1的读端,默认是阻塞模式;如果命名管道的写端打不开,则open()将会一直阻塞,所以另外一个进程必须首先以>
写模式打开该文件FIFO_FILE1,否则会出现死锁*/
printf("start open '%s' for read and it will blocked untill write endpoint opened...\n",FIFO_FILE1);
if((fdr_fifo = open(FIFO_FILE1,O_RDONLY)) < 0)
{
printf("Open fifo[%s] for chat read endpoint failure:%s\n",FIFO_FILE1,strerror(errno));
return -1;
}
printf("start open '%s' for write...\n",FIFO_FILE2);
if((fdw_fifo = open(FIFO_FILE2,O_WRONLY)) < 0)
{
printf("Open fifo[%s] for chat write endpoint failure:%s\n",FIFO_FILE2,strerror(errno));
return -1;
}
}
else
{
/*这里以只写模式打开命名管道FIFO_FILE1的写端,默认是阻塞模式;如果命名管道的读端不打开,则open()将会一直阻塞,因为前一个进程是先以读模式>
打开该管道文件的读端,所以这里必须先以写模式打开该文件的写端,否则将会出现死锁*/
printf("start open '%s' for write and it will blocked untill read endpoint opened..\n",FIFO_FILE1);
if((fdw_fifo = open(FIFO_FILE1,O_WRONLY)) < 0)
{
printf("Open fifo[%s] for chat write endpoint failure:%s\n",FIFO_FILE1,strerror(errno));
return -1;
}
printf("start open '%s' for read...\n",FIFO_FILE2);
if((fdr_fifo = open(FIFO_FILE2,O_RDONLY)) < 0)
{
printf("Open fifo[%s] for chat read endpoint failure:%s\n",FIFO_FILE2,strerror(errno));
return -1;
}
}
printf("start chating with another program now,please input message now:\n");
while( !g_stop )
{
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO,&rdset);
FD_SET(fdr_fifo,&rdset);
/*select()多路复用监听标准输入和作为输入的命名管道读端*/
rv = select(fdr_fifo+1,&rdset,NULL,NULL,NULL);
if(rv < 0)
{
printf("Select get timeout or error:%s\n",strerror(errno));
continue;
}
/*如果是作为输入的命名管道上有数据到来则从管道上读入数据并打印到标准输出上*/
if(FD_ISSET(fdr_fifo,&rdset))
{
memset(buf,0,sizeof(buf));
rv = read(fdr_fifo,buf,sizeof(buf));
if(rv < 0)
{
printf("read data from FIFO get error:%s\n",strerror(errno));
break;
}
else if(0 == rv)
{
printf("Another side of FIFO get closed and program will exit now\n");
break;
}
printf("<--%s",buf);
}
/*如果是作为输入上有数据到来,则从标准输入上读入数据后,将数据写入到作为输出的命名管道的另外一个进程*/
if(FD_ISSET(STDIN_FILENO,&rdset))
{
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
write(fdw_fifo,buf,strlen(buf));
}
}
}
运行结果:
./fifo_chat 0
./fifo_chat 1
通过命名管道,我们成功实现了进程0和进程1聊天的程序。并且,我们也可以在文件系统中看到创建的两个掩藏的命名管道文件(.fifo_chat1和.fifo_chat2),并且大小为0 。
命名socket(Named Socket)又叫Unix域socket,与其他方法不同的是:Socket不仅可以跨网络和不同主机进行进程间通信,还可以在同一主机进行进程间通信。
Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务通信的一种方式。UNIX域数据报服务是可靠的,不会丢失消息,也不会传递出错。他也提供了两类套接字:字节流套接字(类似TCP)和数据报套接字(类似UDP)。
命名socket主要用于实现同一主机不同进程,而普通的TCP/IP网络的socket用于不同主机进程间通信。二者相比较,叫我们来看看命名socket有哪些特点吧!
(1)可命名性: 命名socket允许开发者为其分配一个唯一的名称,而不仅仅依赖于IP地址和端口号来识别。这使得在网络中更容易识别和访问特定的通信端点。命名socket是用路径名来表示协议族,创建的文件类型为s。
(2)持久性: 与普通socket不同,命名socket通常具有持久性,即使通信的两端断开连接,命名socket也能够保持存在。这使得在重新连接时无需重新创建socket,从而节省了时间和资源。
(3)速度快:命名socket仅仅复制数据,并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不产生顺序号,也不需要发送确认报文。
(4)适用性: 命名socket通常用于本地进程间通信(IPC),在同一台计算机上的不同进程之间进行通信。这种通信通常比基于网络的通信更快速、可靠,并且不受网络环境的影响。
(5)安全性: 由于命名socket通常限定在本地计算机上,因此可以通过操作系统提供的权限管理机制来控制对其访问的权限,从而提高通信的安全性。
本篇文章主要讲解命名socket,如果有同学对于普通的TCP/IP网络的socket编程不熟悉,推荐看这篇文章---《APUE学习之socket网络编程》,这篇文章对于socket编程的讲解十分详细!
Socket是操作系统提供给程序员操作网络的接口,根据底层不同的实现方式,通信方式也不同。普通的TCP/IP网络的socket是通过IP地址和端口号来标识的,而命名socket使用文件系统路径名来标识的。所以命名socket和普通的TCP/IP网络的socket只是在创建socket和绑定服务器标识的时候与网络socket有区别,具体讲解如下:
int socket(int domain , int type , int protocol);
说明:该函数的返回值为生成的套接字描述符。
参数说明:
(1)第一个参数domain:这个参数决定了该socket是普通的TCP/IP网络的socket还是命名socket。如果我们想要创建一个普通的TCP/IP网络的socket,则该参数应该为AF_INET;如果我们想要创建一个命名socket,则该参数应该被设为AF_UNIX或者AF_LOCAL。
(2)第二个参数type:上文提到了命名socket提供了两类套接字:字节流套接字和数据报套接字。如果要字节流套接字则设置SOCK_STREAM、如果要数据报套接字则设置SOCK_DGRAM。
(3)第三个参数protocol:通常设为0 。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
这里主要讲解一下第二个参数addr,其余参数和普通的TCP/IP网络的socket一样。第二个参数addr:addr:一个struct sockaddr
结构体的指针,包含要绑定的地址信息。这个地址结构根据地址创建socket时的协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针给内核。
SOCK_STREAM式通信提供了一种可靠的、双向的、基于连接的通信机制,适用于许多网络应用场景。SOCK_STREAM式本地套接字的通信需要双方均具有本地地址,其中服务器端的本地地址需要明确指定。AF_INET指定方法是使用struct sockaddr_in类型的变量,而AF_UNIX指定方法则是使用struct sockaddr_un类型的变量。
接下来叫我们一起来看看struct sockaddr_un类型的变量:
struct sockaddr_un
{
sa_family_t sun_family; /*AF_UNIX*/
char sun_path[UNIX_PATH_MAX]; /*路径名*/
};
再来说一下命名socket的命名方式。socket会根据此命名来创建一个同名的socket文件,客户端连接的时候通过该文件连接到socket服务端。这种方式的弊端是服务端必须对socket文件的路径具备写权限,客户端必须知道socket文件路径,且必须对该路径有读权限。
题目:
用命名socket编写一个服务端和一个客户端,实现客户端与服务端的通信。
代码如下:
服务端:
#include
#include
#include
#include
#include
#include
#include
#define SOCKET_PATH "/tmp/socket.domain"
int main(int argc,char *argv[])
{
int rv = -1;
int listen_fd = -1;
int client_fd = -1;
struct sockaddr_un serv_addr;
struct sockaddr_un cli_addr;
socklen_t cliaddr_len = sizeof(cli_addr);
char buf[1024];
listen_fd = socket(AF_UNIX,SOCK_STREAM,0);
if(listen_fd < 0)
{
printf("create socket failure:%s\n",strerror(errno));
return -1;
}
printf("socket create fd[%d]\n",listen_fd);
if( !access(SOCKET_PATH,F_OK) )
{
unlink(SOCKET_PATH);
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strncpy(serv_addr.sun_path,SOCKET_PATH,sizeof(serv_addr.sun_path)-1);
if(bind(listen_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
{
printf("create socket failure:%s\n",strerror(errno));
unlink(SOCKET_PATH);
return -2;
}
printf("socket[%d] bind on path \"%s\" ok\n",listen_fd,SOCKET_PATH);
listen(listen_fd,13);
while(1)
{
printf("\n Start waiting and accept new client connect...\n");
client_fd = accept(listen_fd,(struct sockaddr *)&cli_addr,&cliaddr_len);
if(client_fd < 0)
{
printf("accept new socket failure:%s\n",strerror(errno));
return -2;
}
memset(buf,0,sizeof(buf));
if((rv = read(client_fd,buf,sizeof(buf))) < 0)
{
printf("Read data from client socket[%d] failure:%s\n",client_fd,strerror(errno));
close(client_fd);
continue;
}
else if(rv == 0)
{
printf("client socket[%d] disconnected\n",client_fd);
close(client_fd);
continue;
}
printf("read %d bytes data from client[%d] and echo it back:'%s'\n",rv,client_fd,buf);
if(write(client_fd,buf,rv) < 0)
{
printf("Write %d bytes data back to client[%d] failure:%s\n",rv,client_fd,strerror(errno));
close(client_fd);
}
sleep(1);
close(client_fd);
}
close(listen_fd);;
}
客户端:
#include
#include
#include
#include
#include
#include
#include
#define SOCKET_PATH "/tmp/socket.domain"
#define MSG_STR "Hello,Unix Domain Socket Server!"
int main(int argc,char *argv[])
{
int conn_fd = -1;
int rv = -1;
char buf[1024];
struct sockaddr_un serv_addr;
conn_fd = socket(AF_UNIX,SOCK_STREAM,0);
if(conn_fd < 0)
{
printf("create socket failure:%s\n",strerror(errno));
return -1;
}
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sun_family = AF_UNIX;
strncpy(serv_addr.sun_path,SOCKET_PATH,sizeof(serv_addr.sun_path)-1);
if(connect(conn_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0)
{
printf("connect to unix domain socket server on \"%s\" failure:%s\n",SOCKET_PATH,strerror(errno));
return 0;
}
printf("connect to unix domain socket server on \"%s\" ok\n",SOCKET_PATH);
if(write(conn_fd,MSG_STR,strlen(MSG_STR)) < 0)
{
printf("write data to unix domain socket server failure:%s\n",strerror(errno));
goto cleanup;
}
memset(buf,0,sizeof(buf));
rv = read(conn_fd,buf,sizeof(buf));
if(rv < 0)
{
printf("Read data from server failure:%s\n",strerror(errno));
goto cleanup;
}
else if(0 == rv)
{
printf("Client connect to server get disconnected\n");
goto cleanup;
}
printf("Read %d bytes data from server:'%s'\n",rv,buf);
cleanup:
close(conn_fd);
}
运行结果:
服务端
客户端
上面实战演练的代码中用到了access()函数,在这里给大家简单讲解一下该函数,让我们先来看一下该函数的原型吧:
#include
int access(const char *filenpath, int mode)
参数说明:
(1)第一个参数filenpath:文件或文件夹的路径,当前目录直接使用文件或文件夹名(使用绝对路径)。
(2)第二个参数mode:
R_OK 只判断是否有读权限
W_OK 只判断是否有写权限
X_OK 判断是否有执行权限
F_OK 只判断是否存在
注意:R_OK、W_OK、X_OK可进行或(|)运算,比如:R_OK|W_OK,即同时判断文件是否具有读写权限。
每一次的尝试,每一次失败,都是通向梦想的一步!加油,致敬每一个学习路上的追梦人!