linux 编程2

文件和记录锁定
共享资源的保护问题是多进程操作系统中一个非常重要的问题。文件锁定的是整个文件,而记录锁定只锁定文件的某一特定部分。
System V 的咨询锁定
System V 的锁函数lockf()具有如下的形式:
#include <unistd.h>
int lockf(int fd, int function, long size);
参数fd 是在文件打开操作中获得的文件描述符;
参数function 可以取如下的参数值:
    F_ULOCK 为一个先前锁定的区域解锁
    F_LOCK 锁定一个区域
    F_TLOCK 测试并锁定一个区域
    F_TEST 测试一个区域是否已经上锁。
参数size 指明了从文件当前位置开始的一段连续锁定区域的长度,当size 为0 时,锁定记录将由当前位置一直扩展到文件尾。
函数lockf()既可以用来上锁有可以用来测试是否已经赏了锁。如果lockf 的参数function为F_LOCK 指定文件的对应区域已被其它进程锁定,那么lockf 的调用进程将被阻塞直到该区域解锁。上述情况我们称为阻塞。如果在调用lockf()时把参数设为F_TLOVK,那么当被测试的区域上了锁时,lockf 便会立即返回-1,出错返回码errno 将为EAGAIN,它是一个非阻塞调用。
if(!lockf(fd,F_TEST,size))
{
    rc==lockf(fd,F_LOCK,size);
    …
    …
}
上面这段代码看上去好像是非阻塞调用,但是如果当运行此代码段的进程在测试到对应文件没有被锁定时,又有另一个进程被操作系统调度占有CPD,它将同样测试出文件未被锁定,然后对共享文件上锁。当后继进程在对锁文件操作时,再一次被操作系统调度的第一个进程,其锁定文件的操作将仍然是一个阻塞性调用。因此为了实现非阻塞调用,我们必须使用F_TLOCK 参数的lockf()调用。有个锁函数lockf()之后,我们便可以完善前面的上锁my_lock()和解锁my_unlock() 函数,防止共享文件访问中的混乱情况。下面的上锁函数采用的是阻塞调用。
#include <unistd.h>
my_lock(int fd)
{
    /* 将文件指针移回文件头 */
    lseek(fd,0L,0);
    /* 锁定整个文件 */
    if (lockf(fd,F_LOCK,0L)==-1)
    {
        perror("can't F_LOCK");
        exit(1);
    }
}

my_unlock(int fd)
{
    lseek(fd,0L,0);
    if(lockf(fd,F_ULOCK,0L)==-1)
    {
        perror("can't F_UNLOCK");
        exit(1);
    }
}
BSD 的咨询式锁定
4.3 BSD UNIX 操作系统提供了如下形式的调用来锁定和解锁一个文件:
#include <sys/file.h>
int flock(int fd, int operation);
调用flock 有两个参数:
参数fd 是一个已打开文件的文件描述符;
参数operation 可设定为下述各值:
    LOCK_SH 共享锁
    LOCK_EX 互斥锁
    LOCK_UN 解锁
    LOCK_NB 当文件已被锁定时不阻塞
BSD UNIX 使用flock()来请求对指定文件的咨询式锁定和解锁。BSD 的咨询锁
有共享锁和互斥锁两种。在任一给定时刻,多个进程可以用于属于同一文件的共享锁,但是某共享文件不能同时具有多个互斥锁或存在共享锁和互斥锁共存的情况。如果锁定成功,flock 将返回零,否则返回-1。
flock()允许的锁操作有下列几种:
    LOCK_SH 阻塞性共享锁
    LOCK_EX 阻塞性互斥锁
    LOCK_SH | LOCK_NB 非阻塞性共享锁
    LOCK_EX | LOCK_NB 非阻塞性互斥锁
    LOCK_UN 解锁
当flock()采用非阻塞锁定操作时,对已锁定文件的锁定将使该调用失败返回,其出错码为EWOULDBLOCK。
#include <sys/file.h>
my_flock(int fd)
{
    if (flock(fd,LOCK_EX))==-1)
    {
        perror(“can LOCK_EX”);
        exit(1);
    }
}
my_unload(fd)
{
    if (flock(fd,LOCK_UN)==-1)
    {
        perror(“can’t LOCK_UN”);
        exit(1);
    }
}
有了这个上锁函数和my_lock()和解锁函数my_unlock()之后,让我们再回过头来看看前面的a.out&命令的运行结果:
    pid=5894,seq#=1
    pid=5894,seq#=2
    pid=5894,seq#=3
    pid=5894,seq#=4
    pid=5894,seq#=5
    pid=5895,seq#=6
    pid=5895,seq#=7
    pid=5895,seq#=8
    pid=5895,seq#=9
    pid=5895,seq#=10
该结果完全体现了对共享序号文件“seqno”的理想共享操作。
Linux 支持上面的两种锁定方式,所以可以根据不同的实际情况选用不同的锁定
方式。以上的两种锁定方式有以下的不同:
1.System V的锁定方式是记录锁定,可以指定锁定的范围。而BSD 的锁定方式是文件锁定,只能指定锁定文件。
2.System V 的锁定是每个进程所独有的,可以用于父子进程间的共享锁定。而BSD的锁定方式是可以继承的,父子进程间使用的是同一锁定的,所以不能用于父子进程间的文件共享锁定。

dup()函数
有时候我们需要将子进程当中的管道的句柄定向到标准I/O(stdin/stdout)上去。这样,在子进程中使用exec()函数调用外部程序时,这个外部程序就会将管道作为它的输入/输出。这个过程可以用系统函数dup()来实现。其函数原型如下:
系统调用: dup();
函数声明: int dup( int oldfd );
返回值: new descriptor on success-1 on error:
errno = EBADF (oldfd is not a valid descriptor)
        EBADF (newfd is out of range)
        EMFILE (too many descriptors for the process)
注意: 旧句柄没有被关闭,新旧两个句柄可以互换使用
虽然原句柄和新句柄是可以互换使用的,但为了避免混淆,我们通常会将原句柄关闭(close)。同时要注意,在dup()函数中我们无法指定重定向的新句柄,系统将自动使用未被使用的最小的文件句柄(记住,句柄是一个整型量)作为重定向的新句柄。请看下面的例子:
    pipe(fd);
    childpid = fork();
    if(childpid == 0)
    {
        /* 关闭子进程的文件句柄0(stdin) */
        close(0);
        /* 将管道的读句柄定义到stdin */
        dup(fd[0]);
        execlp(“sort”, “sort”, NULL);
    }    
在上例中巧妙的利用了dup()函数的特性。因为文件句柄0(stdin)被关闭了,对dup函数的调用就将管道读句柄fd[0]定向到了stdin(因为句柄0是最小的未用句柄)。然后我们调用execlp 函数,用外部过程sort覆盖了子进程的代码。现在,我们在父进程里向管道写入的任何数据都将自动被sort 接受进行排序    。
系统调用: dup2();
函数声明: int dup2( int oldfd, int newfd );
返回值: new descriptor on success-1 on error:
        errno = EBADF (oldfd is not a valid descriptor)
                EBADF (newfd is out of range)
                EMFILE(too many descriptors for the process)
注意: 旧句柄将被dup2()自动关闭
显然,原来的close以及dup这一套调用现在全部由dup2来完成。这样不仅简便了程序,它保证了操作的独立性和完整性,不会被外来的信号所中断。
popen()/pclose()函数(把fork、exec 和dup()结合的函数)
库函数: popen();
函数声明: FILE *popen ( char *command, char *type);
返回值: new file stream on success
    NULL on unsuccessful fork() or pipe() call
    NOTES: creates a pipe, and performs fork/exec operations using "command"
popen()函数首先调用pipe()函数建立一个管道,然后它用fork()函数建立一个子进程,运行一个shell 环境,然后在这个shell 环境中运行"command"参数指定的程序。数据在管道中流向由"type"参数控制。这个参数可以是"r"或者"w",分别代表读和写。需要注意的是,"r"和"w"两个参数不能同时使用!在Linux 系统中,popen 函数将只使用"type"参数中第一个字符,也就是说,使用"rw"和"r"作为"type"参数的效果是一样的,管道将只打开成读状态。
使用popen 打开的管道必须用pclose()函数来关闭。
下面是一个使用popen/pclose 的例子:
#include <stdio.h>
#define MAXSTRS 5
int main(void)
{
    int cntr;
    FILE *pipe_fp;
    char *strings[MAXSTRS] = { "roy", "zixia", "gouki","supper", "mmwan"};
    /* 用popen 建立管道 */
    if (( pipe_fp = popen("sort", "w")) == NULL)
    {
        perror("popen");
        exit(1);
    }
    /* Processing loop */
    for(cntr=0; cntr<MAXSTRS; cntr++)
    {
        fputs(strings[cntr], pipe_fp);
        fputc('\n', pipe_fp);
    }
    /* 关闭管道 */
    pclose(pipe_fp);
    return(0);
}
下面是一个稍微复杂一点的例子,在里面建立了两个管道:
#include <stdio.h>
int main(void)
{
    FILE *pipein_fp, *pipeout_fp;
    char readbuf[80];
    /* 用popen 建立一个通向"ls:的读管道*/
    if (( pipein_fp = popen("ls", "r")) == NULL)
    {
        perror("popen");
        exit(1);
    }
    /* 用popen 建立一个通向"sort"的写管道 */
    if (( pipeout_fp = popen("sort", "w")) == NULL)
    {
        perror("popen");
        exit(1);
    }
    /* 进程循环 */
    while(fgets(readbuf, 80, pipein_fp))
    fputs(readbuf, pipeout_fp);
    /* 关闭打开的管道 */
    pclose(pipein_fp);
    pclose(pipeout_fp);
    return(0);
}
popen()和fopen()混合使用的例子
int main(int argc, char *argv[])
{
    FILE *pipe_fp, *infile;
    char readbuf[80];
    if( argc != 3) {
        fprintf(stderr, "USAGE: popen3 [command] [filename]\n");
        exit(1);
    }
    /* 打开输入文件 */
    if (( infile = fopen(argv[2], "rt")) == NULL)
    {
        perror("fopen");
        exit(1);
    }
    /* 建立写管道 */
    if (( pipe_fp = popen(argv[1], "w")) == NULL)
    {
        perror("popen");
        exit(1);
    }
    /* Processing loop */
    do{
        fgets(readbuf, 80, infile);
        if(feof(infile)) break;
        fputs(readbuf, pipe_fp);
    } while(!feof(infile));
    fclose(infile);
    pclose(pipe_fp);
    return(0);
}
ipcs 命令
ipcs 命令在终端显示系统内核的IPC 对象状况。
ipcs –q 只显示消息队列
ipcs –m 只显示共享内存
ipcs –s 只显示信号量
ipcrm <msg | sem | shm> <IPC ID>
ipcrm 后面的参数指定要删除的IPC对象类型,分别为消息队列(msg)、信号量(sem)和共享内存(shm)。
消息队列
1.ipc_perm
系统使用ipc_perm 结构来保存每个IPC 对象权限信息。在Linux 的库文件linux/ipc.h
中,它是这样定义的:
struct ipc_perm
{
    key_t key;
    ushort uid; /* owner euid and egid */
    ushort gid;
    ushort cuid; /* creator euid and egid */
    ushort cgid;
    ushort mode; /* access modes see mode flags below */
    ushort seq; /* slot usage sequence number */
};
2.msgbuf
msgbuf 结构给了我们一个这类数据类型的基本结构定义。
在Linux 的系统库linux/msg.h 中,它是这样定义的:
/* message buffer for msgsnd and msgrcv calls */
struct msgbuf {
    long mtype; /* type of message */
    char mtext[1]; /* message text */
};
它有两个成员:
mtype 是一个正的长整型量,通过它来区分不同的消息数据类型。
mtext 是消息数据的内容。
需要注意的是,虽然消息的内容mtext 在msgbuf 中只是一个字符数组,但事实上,在
我们定义的结构中,和它对应的部分可以是任意的数据类型,甚至是多个数据类型的集合。
比如我们可以定义这样的一个消息类型:
struct my_msgbuf {
    long mtype; /* Message type */
    long request_id; /* Request identifier */
    struct client info; /* Client information structure */
};
在这里,与mtext 对应的是两个数据类型,其中一个还是struct 类型。
不过,虽然没有类型上的限制,但Linux系统还是对消息类型的最大长度做出了限制。在Linux 的库文件linux/msg.h 中定义了每个msgbuf 结构的最大长度:
#define MSGMAX 4056 /* <= 4056 */ /* max size of message (bytes) */
也即是说,包括mtype所占用的4个字节,每个msgbuf结构最多只能只能占用4056
字节的空间。
3.msg
消息队列在系统内核中是以消息链表的形式出现的。而完成消息链表每个节点结构定
义的就是msg 结构。它在Linux 的系统库linux/msg.h 中的定义是这样的:
/* one msg structure for each message */
struct msg {
    struct msg *msg_next; /* next message on queue */
    long msg_type;
    char *msg_spot; /* message text address */
    time_t msg_stime; /* msgsnd time */
    short msg_ts; /* message text size */
};
msg_next:成员是指向消息链表中下一个节点的指针,依靠它对整个消息链表进行访问。
msg_type :和msgbuf 中mtype 成员的意义是一样的。
msg_spot:成员指针指出了消息内容(就是msgbuf结构中的mtext)在内存中的位置。
msg_ts :成员指出了消息内容的长度。
4.msgqid_ds
msgqid_ds 结构被系统内核用来保存消息队列对象有关数据。内核中存在的每个消息
队列对象系统都保存一个msgqid_ds 结构的数据存放该对象的各种信息。在Linux 的库文
件linux/msg.h 中,它的定义是这样的:
/* one msqid structure for each queue on the system */
struct msqid_ds {
struct ipc_perm msg_perm;
struct msg *msg_first; /* first message on queue */
struct msg *msg_last; /* last message in queue */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* last change time */
struct wait_queue *wwait;
struct wait_queue *rwait;
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /* number of messages in queue */
unsigned short msg_qbytes; /* max number of bytes on queue */
__kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
__kernel_ipc_pid_t msg_lrpid; /* last receive pid */
};
其中,
msg_perm 成员保存了消息队列的存取权限以及其他一些信息(见上面关于ipc_perm
结构的介绍)。
msg_first 成员指针保存了消息队列(链表)中第一个成员的地址。
msg_last 成员指针保存了消息队列中最后一个成员的地址。
msg_stime 成员保存了最近一次队列接受消息的时间。
msg_rtime 成员保存了最近一次从队列中取出消息的时间。
msg_ctime 成员保存了最近一次队列发生改动的时间
wwait 和rwait 是指向系统内部等待队列的指针。
msg_cbytes 成员保存着队列总共占用内存的字节数。
msg_qnum 成员保存着队列里保存的消息数目。
msg_qbytes 成员保存着队列所占用内存的最大字节数。
msg_lspid 成员保存着最近一次向队列发送消息的进程的pid。
msg_lrpid 成员保存着最近一次从队列中取出消息的进程的pid。

msgctl()
通过msgctl()函数,可以直接控制消息队列的行为。它在系统库linux/msg.h 中的定义是这样的:
系统调用: msgctl()
函数声明: int msgctl ( int msgqid, int cmd, struct msqid_ds *buf )
返回值: 0 on success
-1 on error: errno = EACCES (No read permission and cmd is IPC_STAT)
EFAULT (Address pointed to by buf is invalid with
IPC_SET and IPC_STAT commands)
EIDRM (Queue was removed during retrieval)
EINVAL (msgqid invalid, or msgsz less than 0)
EPERM (IPC_SET or IPC_RMID command was
issued, but calling process does not have
write (alter) access to the queue)
函数的第一个参数msgqid 是消息队列对象的标识符。
第二个参数是函数要对消息队列进行的操作,它可以是:
IPC_STAT
取出系统保存的消息队列的msqid_ds 数据,并将其存入参数buf 指向的msqid_ds 结构
中。
IPC_SET
设定消息队列的msqid_ds 数据中的msg_perm 成员。设定的值由buf 指向的msqid_ds
结构给出。
IPC_EMID
将队列从系统内核中删除。
这三个命令的功能都是明显的,所以就不多解释了。唯一需要强调的是在IPC_STAT
命令中队列的msqid_ds 数据中唯一能被设定的只有msg_perm 成员,其是ipc_perm 类型的
数据。而ipc_perm 中能被修改的只有mode,pid 和uid 成员。其他的都是只能由系统来设定
的。
最后我们将使用msgctl()函数来开发几个封装函数作为本节的例子:
IPC_STAT 的例子:
int get_queue_ds( int qid, struct msgqid_ds *qbuf )
{
if( msgctl( qid, IPC_STAT, qbuf) == -1)
{
return(-1);
}
return(0);
}
IPC_SET 的例子:
int change_queue_mode( int qid, char *mode )
{
    struct msqid_ds tmpbuf;
    /* Retrieve a current copy of the internal data structure */
    get_queue_ds( qid, &tmpbuf);
    /* Change the permissions using an old trick */
    sscanf(mode, "%ho", &tmpbuf.msg_perm.mode);
    /* Update the internal data structure */
    if( msgctl( qid, IPC_SET, &tmpbuf) == -1)
    {
        return(-1);
    }
    return(0);
}
IPC_RMID 的例子:
int remove_queue( int qid )
{
    if( msgctl( qid, IPC_RMID, 0) == -1)
    {
    return(-1);
    }
    return(0);
}
信号量(Semaphores)
信号量简单的说就是用来控制多个进程对共享资源使用的计数器。它经常被用作一种锁定保护机制,当某个进程在对资源进行操作时阻止其它进程对该资源的访问。
1.sem
Sem 结构在Linux 系统库linux/sem.h 中的定义是这样的:
/* One semaphore structure for each semaphore in the system. */
struct sem {
short sempid; /* pid of last operation */
ushort semval; /* current value */
ushort semncnt; /* num procs awaiting increase in semval */
ushort semzcnt; /* num procs awaiting semval = 0 */
};
其中,
sem_pid 成员保存了最近一次操作信号量的进程的pid 。
sem_semval 成员保存着信号量的计数值。
sem_semncnt 成员保存着等待使用资源的进程数目。
sem_semzcnt 成员保存等待资源完全空闲的的进程数目。
2.semun
semun 联合在senctl()函数中使用,提供senctl()操作所需要的信息。它在Linux 系统
linux/sem.h 中的定义是这样的:
/* arg for semctl system calls. */
union semun {
    int val; /* value for SETVAL */
    struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
    ushort *array; /* array for GETALL & SETALL */
    struct seminfo *__buf; /* buffer for IPC_INFO */
    void *__pad;
};
3.sembuf
sembuf 结构被semop()函数(后面回讲到)用来定义对信号量对象的基本操作。它在
linux/sem.h 中是这样定义的:
/* semop system calls takes an array of these. */
struct sembuf {
    unsigned short sem_num; /* semaphore index in array */
    short sem_op; /* semaphore operation */
    short sem_flg; /* operation flags */
};
其中,
sem_num 成员为接受操作的信号量在信号量数组中的序号(数组下标)。
sem_op 成员定义了进行的操作(可以是正、负和零)。
sem_flg 是控制操作行为的标志。
如果sem_op是负值,就从指定的信号量中减去相应的值。这对应着获取信号量所监控的资源的操作。如果没有在sem_flg指定IPC_NOWAIT标志,那么,如果现有的信号量数值小于sem_op的绝对值(表示现有的资源少于要获取的资源),调用semop()函数的进程就回被阻塞直到信号量的数值大于sem_op的绝对值(表示有足够的资源被释放)。
如果sem_op是正值,就在指定的信号量中加上相应的值。这对应着释放信号量所监控的资源的操作。
如果sem_op是零,那么调用semop函数的进程就会被阻塞直到对应的信号量值为零。这种操作的实质就是等待信号量所监控的资源被全部使用。利用这种操作可以动态监控资源的使用并调整资源的分配,避免不必要的等待。
4.semid_qs
和msgqid_ds 类似,semid_qs 结构被系统用来储存每个信号量对象的有关信息。它在
Linux 系统库linux/sem.h 中是这样定义的:
/* One semid data structure for each set of semaphores in the system. */
struct semid_ds {
    struct ipc_perm sem_perm; /* permissions .. see ipc.h */
    __kernel_time_t sem_otime; /* last semop time */
    __kernel_time_t sem_ctime; /* last change time */
    struct sem *sem_base; /* ptr to first semaphore in array */
    struct sem_queue *sem_pending; /* pending operations to be processed */
    struct sem_queue **sem_pending_last; /* last pending operation */
    struct sem_undo *undo; /* undo requests on this array *
    /
    unsigned short sem_nsems; /* no. of semaphores in array */
};
其中,
sem_perm 成员保存了信号量对象的存取权限以及其他一些信息
sem_otime 成员保存了最近一次semop()操作的时间。
sem_ctime 成员保存了信号量对象最近一次改动发生的时间。
sem_base 指针保存着信号量数组的起始地址。
sem_pending 指针保存着还没有进行的操作。
sem_pending_last 指针保存着最后一个还没有进行的操作。
sem_undo 成员保存了undo 请求的数目。
sem_nsems 成员保存了信号量数组的成员数目。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值