我正在创建一个可以读取命令的小shell。当我运行我的程序并输入:"cat file.txt > file2.txt"
时,它会创建文件,然后它会卡在行:if(execvp(structVariables->argv[0], argv) < 0).
(等待输入/输出??)。如果我用ctrl + d结束程序,我可以在我的文件夹中看到文件已创建,但没有写入任何内容。 (dupPipe用于处理由于上述问题而尚未使用的更多命令)
if((pid = fork()) < 0)
{
perror("fork error");
}
else if(pid > 0) // Parent
{
if(waitpid(pid,NULL,0) < 0)
{
perror("waitpid error");
}
}
else // Child
{
int flags = 0;
if(structVariables->outfile != NULL)
{
flags = 1; // Write
redirect(structVariables->outfile, flags, STDOUT_FILENO);
}
if(structVariables->infile != NULL)
{
flags = 2; // Read
redirect(structVariables->infile, flags, STDIN_FILENO);
}
if(execvp(structVariables->argv[0], argv) < 0)
{
perror("execvp error");
exit(EXIT_FAILURE);
}
}
我在程序中使用的两个函数如下所示: dupPipe和重定向
int dupPipe(int pip[2], int end, int destinfd)
{
if(end == READ_END)
{
dup2(pip[0], destinfd);
close(pip[0]);
}
else if(end == WRITE_END)
{
dup2(pip[1], destinfd);
close(pip[1]);
}
return destinfd;
}
int redirect(char *filename, int flags, int destinfd)
{
int newfd;
if(flags == 1)
{
if(access(filename, F_OK) != -1) // If file already exists
{
errno = EEXIST;
printf("Error: %s\n", strerror(errno));
return -1;
}
newfd = open(filename, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
else if(flags == 2)
{
newfd = open(filename, O_RDONLY);
if(newfd == -1)
{
perror("Open for read failed");
return -1;
}
}
else
return -1;
if(dup2(newfd, destinfd) == -1)
{
perror("dup2 failed");
close(newfd);
return -1;
}
if(newfd != destinfd)
{
close(newfd);
}
return destinfd;
}
答案 0 :(得分:7)
您似乎正在尝试编写shell来运行从输入读取的命令(如果不是这样;请编辑您的问题,因为它不清楚)。
我不确定为什么你认为管道在cat file.txt > file2.txt
之类的命令中被使用,但无论如何,它们不是。让我们看看在bash这样的shell中输入cat file.txt > file2.txt
时会发生什么:
cat(1)
将运行。file2.txt
进行编写(稍后会详细介绍)。open(2)
成功,子进程会将新打开的文件描述符复制到stdout
(因此stdout
将有效地指向与file2.txt
相同的文件表条目) cat(1)
函数之一来执行exec()
。参数file.txt
传递给cat(1)
,因此cat(1)
将打开file.txt
并阅读所有内容,将其内容复制到stdout
(重定向到{{1} }})。file2.txt
完成执行并终止,这会导致任何打开的文件描述符被关闭和刷新。在cat(1)
终止时,cat(1)
是file2.txt
的副本。如您所见,I / O重定向中未使用管道。管道是一种进程间通信机制,用于将进程的输出提供给另一个进程的输入。你只有一个进程在这里运行(file.txt
),所以为什么你甚至需要管道?
这意味着您应该使用cat
redirect()
作为STDOUT_FILENO
(而不是管道通道)调用destinfd
进行输出重定向。同样,输入重定向应使用redirect()
调用STDIN_FILENO
。这些常量在unistd.h
中定义,因此请确保包含该标题。
如果exec()
失败,您可能想要退出孩子,否则您将运行2个shell过程副本。
最后但并非最不重要的是,您不应该使输入或输出重定向独占。可能是用户想要输入和输出重定向的情况。因此,在进行I / O重定向时,我只使用2个独立的ifs而不是else if
。
考虑到这一点,您发布的主要代码应该类似于:
if((pid = fork()) < 0)
{
perror("fork error");
}
else if(pid > 0) // Parent
{
if(waitpid(pid,NULL,0) < 0)
{
perror("waitpid error");
}
}
else // Child
{
int flags = 0;
if(structVariables->outfile != NULL)
{
flags = 1; // Write
// We need STDOUT_FILENO here
redirect(structVariables->outfile, flags, STDOUT_FILENO);
}
if(structVariables->infile != NULL)
{
flags = 2; // Read
// Similarly, we need STDIN_FILENO here
redirect(structVariables->infile, flags, STDIN_FILENO);
}
// This line changed; see updated answer below
if(execvp(structVariables->argv[0], structVariables->argv) < 0)
{
perror("execvp error");
// Terminate
exit(EXIT_FAILURE);
}
}
正如另一个答案中所提到的,您的redirect()
函数容易出现竞争条件,因为文件存在检查和实际文件创建之间有一个时间窗口,其他进程可以创建该文件(这称为TOCTTOU错误:检查时间到使用时间)。您应该使用O_CREAT | O_EXCL
以原子方式测试是否存在并创建文件。
另一个问题是你总是关闭newfd
。如果newfd
和destinfd
由于某种原因恰好相同怎么办?然后你将错误地关闭文件,因为如果你传入两个相同的文件描述符,dup2(2)
本质上是一个无操作。即使您认为这种情况永远不会发生,在关闭原始文件之前,首先检查重复的fd是否与原始fd不同是一种好习惯。
以下是解决这些问题的代码:
int redirect(char *filename, int flags, int destinfd)
{
int newfd;
if(flags == 1)
{
newfd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0666);
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
else if(flags == 2)
{
newfd = open(filename, O_RDONLY);
if(newfd == -1)
{
perror("Open for read failed");
return -1;
}
}
else
return -1;
if(dup2(newfd, destinfd) == -1)
{
perror("dup2 failed");
close(newfd);
return -1;
}
if (newfd != destinfd)
close(newfd);
return destinfd;
}
考虑使用0666
替换上方open(2)
中的S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
(确保包含sys/stat.h
和fcntl.h
)。您可能希望使用#define
来使其更清晰,但如果您这样做,我仍然认为它更好,更具描述性,而不是硬编码一些幻数(虽然这是主观的)。
我不会对dupPipe()
发表评论,因为在这个问题中不需要/使用它。 I / O重定向就是您所需要的。如果您想将讨论扩展到管道,请随时编辑问题或创建另一个问题。
<强>更新强>
好的,现在我已经看了完整的源代码,我还有几个评论。
cat(1)
悬挂的原因是:
if (execvp(structVariables->argv[0], argv) < 0)
execvp(2)
的第二个参数应该是structVariables->argv
,不是 argv
,因为argv
是shell程序的参数数组, (通常)是空的。将空参数列表传递给cat(1)
会使其从stdin
而不是文件中读取,这就是为什么它似乎挂起 - 它等待您提供输入。因此,请继续使用以下内容替换该行:
if (execvp(structVariables->argv[0], structVariables->argv) < 0)
这解决了您的一个问题:cat < file.txt > file2.txt
之类的内容现在可以使用(我测试了它)。
关于管道重定向
所以现在我们需要处理管道重定向。每次我们在命令行上看到|
时都会发生管道重定向。让我们通过一个例子来了解当我们输入ls | grep "file.txt" | sort
时在幕后发生的事情。了解这些步骤非常重要,这样您就可以建立一个准确的系统工作心理模型;没有这样的愿景,你就不会真正理解这个实现:
ls
命令,grep
命令和sort
命令。 shell会分叉并调用子项上的七个exec()
函数之一来运行ls
。现在,请记住,管道意味着程序的输出是下一个的输入,所以在exec()
之前,shell必须创建一个管道。即将运行ls(1)
的子进程会在dup2(2)
之前调用exec()
,以便将管道的写入通道复制到stdout
。同样,父进程调用dup2(2)
将管道的读取通道复制到stdin
。理解这一步非常重要:因为父管将管道的读端复制到stdin
,然后我们接下来做的任何事情(例如再次执行fork以执行更多命令)将始终从管道读取输入。所以,此时,我们ls(1)
写入stdout
,它被重定向到由shell的父进程读取的管道。
shell现在将执行grep(1)
。同样,它要求新进程执行grep(1)
。请记住,文件描述符是通过fork继承的,并且父shell的进程将stdin
绑定到连接到ls(1)
的管道的读取端,因此新的子进程是关于执行grep(1)
将&#34;自动&#34;从那根烟斗上读!但是等等,还有更多! shell知道管道中还有另一个进程(sort
命令),因此在执行grep之前(以及在分叉之前),shell创建另一个管道来连接{的输出{1}}输入grep(1)
。然后,它重复相同的步骤:在子进程上,管道的写入通道被复制到sort(1)
上。在父级中,管道的读取通道被复制到stdout
上。同样,重要的是要真正理解这里发生的事情:即将执行的进程stdin
已经从连接到grep(1)
的管道读取其输入,现在它已连接到输出将提供ls(1)
的管道。所以sort(1)
实际上是从管道中读取并写入管道。 OTOH,父shell进程将最后一个管道的读取通道复制到grep(1)
,有效地放弃&#34;放弃&#34;从阅读stdin
的输出(因为ls(1)
无论如何都会处理它),而是更新输入流以从grep(1)
读取结果。
最后,shell看到grep(1)
是最后一个命令,所以它只是forks + execs sort(1)
。结果写入sort(1)
,因为我们从未在shell进程中更改stdout
,但是输入是从连接stdout
到grep(1)
的管道中读取的,因为我们的操作是第3步。
那么这是如何实现的呢?
简单:只要有多个命令要处理,我们就会创建一个管道和分支。在孩子身上,我们关闭管道的读取通道,将管道的写入通道复制到sort(1)
,然后调用七个stdout
函数中的一个。在父级上,我们关闭管道的写入通道,并将管道的读取通道复制到exec()
。
当只剩下一个命令要处理时,我们只需fork + exec,而不创建管道。
只有最后一个细节需要澄清:在启动stdin
重定向方之前,我们需要存储对原始shell标准输入的引用,因为我们(可能)会多次更改它一路走来。如果我们没有保存它,我们可能会丢失对原始pipe(2)
文件的引用,然后我们将无法再读取用户输入!在代码中,我通常使用stdin
和fcntl(2)
(请参阅F_DUPFD_CLOEXEC
)执行此操作,以确保在子进程中执行命令时关闭描述符(通常是使用时留下打开文件描述符的不良做法。)
此外,shell进程需要在管道中的 last 进程上man 2 fcntl
。如果你考虑它,它是有道理的:管道固有地同步管道中的每个命令;只有当最后一个命令从管道读取wait(2)
时,才假定命令集结束(也就是说,我们知道只有当所有数据都流过整个管道时才会完成)。如果shell没有等待最后一个进程,而是等待管道中间(或开头)的其他进程,它会过早地返回命令提示符并让其他命令仍在运行在后台 - 不是智能移动,因为用户期望shell在等待更多之前完成当前作业的执行。
所以...这是很多信息,但你理解它是非常重要的。所以修改后的主要代码在这里:
EOF
关于int saved_stdin = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0);
if (saved_stdin < 0) {
perror("Couldn't store stdin reference");
break;
}
pid_t pid;
int i;
/* As long as there are at least two commands to process... */
for (i = 0; i < n-1; i++) {
/* We create a pipe to connect this command to the next command */
int pipefds[2];
if (pipe(pipefds) < 0) {
perror("pipe(2) error");
break;
}
/* Prepare execution on child process and make the parent read the
* results from the pipe
*/
if ((pid = fork()) < 0) {
perror("fork(2) error");
break;
}
if (pid > 0) {
/* Parent needs to close the pipe's write channel to make sure
* we don't hang. Parent reads from the pipe's read channel.
*/
if (close(pipefds[1]) < 0) {
perror("close(2) error");
break;
}
if (dupPipe(pipefds, READ_END, STDIN_FILENO) < 0) {
perror("dupPipe() error");
break;
}
} else {
int flags = 0;
if (structVariables[i].outfile != NULL)
{
flags = 1; // Write
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2; // Read
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
/* Child writes to the pipe (that is read by the parent); the read
* channel doesn't have to be closed, but we close it for good practice
*/
if (close(pipefds[0]) < 0) {
perror("close(2) error");
break;
}
if (dupPipe(pipefds, WRITE_END, STDOUT_FILENO) < 0) {
perror("dupPipe() error");
break;
}
if (execvp(structVariables[i].argv[0], structVariables[i].argv) < 0) {
perror("execvp(3) error");
exit(EXIT_FAILURE);
}
}
}
if (i != n-1) {
/* Some error caused an early loop exit */
break;
}
/* We don't need a pipe for the last command */
if ((pid = fork()) < 0) {
perror("fork(2) error on last command");
}
if (pid > 0) {
/* Parent waits for the last command to execute */
if (waitpid(pid, NULL, 0) < 0) {
perror("waitpid(2) error");
}
} else {
int flags = 0;
/* Execute last command. This will read from the last pipe we set up */
if (structVariables[i].outfile != NULL)
{
flags = 1; // Write
if (redirect(structVariables[i].outfile, flags, STDOUT_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (structVariables[i].infile != NULL)
{
flags = 2; // Read
if (redirect(structVariables[i].infile, flags, STDIN_FILENO) < 0) {
perror("redirect() error");
exit(EXIT_FAILURE);
}
}
if (execvp(structVariables[i].argv[0], structVariables[i].argv) < 0) {
perror("execvp(3) error on last command");
exit(EXIT_FAILURE);
}
}
/* Finally, we need to restore the original stdin descriptor */
if (dup2(saved_stdin, STDIN_FILENO) < 0) {
perror("dup2(2) error when attempting to restore stdin");
exit(EXIT_FAILURE);
}
if (close(saved_stdin) < 0) {
perror("close(2) failed on saved_stdin");
}
的最后评论:
dupPipe()
和dup2(2)
都可能返回错误;你应该检查一下并采取相应的行动(即通过返回-1将错误传递给调用堆栈)。close(2)
是end
还是READ_END
,如果不是,则返回错误(而不是返回WRITE_END
,无论如何,给调用者代码一个错误的成功感)以下是我如何改进它:
destinfd
玩弄你的贝壳!
答案 1 :(得分:2)
除非出现错误,否则execvp不会返回。
因此,原始程序(通常)不会执行execvp()调用之后的代码
正常的代码序列是:
1) fork()
2) if child then call execvp();
3) if parent ....
答案 2 :(得分:0)
open()
如果redirect()
,您flags == 1
错误地使用了 if(flags == 1)
{
if(access(filename, F_OK) != -1) // If file already exists
{
errno = EEXIST;
printf("Error: %s\n", strerror(errno));
return -1;
}
newfd = open(filename, O_CREAT, O_WRONLY);
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
:
newfd = open(filename, O_CREAT, O_WRONLY);
在O_WRONLY
中,mode
(错误地)用于代替open()
的{{1}}参数,而不是flags
中的 if(flags == 1)
{
if(access(filename, F_OK) != -1) // If file already exists
{
errno = EEXIST;
printf("Error: %s\n", strerror(errno));
return -1;
}
newfd = open(filename, O_CREAT | O_WRONLY, mode); //whatever mode you want, but remember umask.
if(newfd == -1)
{
perror("Open for write failed");
return -1;
}
}
:
access()
此外,检查以前存在的文件是否有效,另一个程序可以在open()
之后和open(filename, O_CREAT | O_EXCL, mode)
之前创建文件。使用 public QueryPage<Merchant> getMerchants(Integer supermerchantId, Geometry point, Double maxDistance,
String keyword, List<Integer> businessTypes, Integer numPage,
Integer sizePage) {
QMerchant merchant = QMerchant.merchant;
QMerchantBusinessType merchantBusinessType= QMerchantBusinessType.merchantBusinessType;
JPAQuery query = new JPAQuery(entityManager);
query = query.from(merchant)
.innerJoin(merchant.merchantBusinessTypes, merchantBusinessType);
// Master merchant quickFix
query = query.where(merchant.idSupermerchant.isNotNull());
if (supermerchantId!=null){
query = query.where(merchant.idSupermerchant.mercId.eq(supermerchantId));
}
if(point != null && maxDistance != null){
query = query.where(merchant.point.distance(point).lt(maxDistance));
}
if(keyword!=null){
query = query.where(merchant.mercName.containsIgnoreCase(keyword));
}
if(businessTypes!=null){
query = query.where(merchantBusinessType.businessType.id.in(businessTypes).and(merchantBusinessType.merchant.eq(merchant)));
}
QueryPage<Merchant> res = new QueryPage<>();
JPAQuery queryCount = query.clone(entityManager);
res.setTotalElements(queryCount.distinct().count());
if(point != null) {
LOGGER.debug(point.toString());
query = query.orderBy(merchant.point.distance(point).asc());
} else {
query = query.orderBy(merchant.mercName.asc());
}
Long offset = new Long((numPage) * sizePage);
query = query.distinct();
query = query.offset(offset).limit(sizePage); // distinct()
LOGGER.debug("=== QUERY:");
LOGGER.debug(query.toString());
List<Merchant> merchants = query.list(merchant);
res.setContent(merchants);
res.setPageNumber(numPage);
res.setPageSize(sizePage);
return res;
}
以原子方式创建和打开文件。